7

我最近遇到了意外的代码优化,想检查一下我对所观察到的内容的解释是否正确。以下是该情况的一个非常简化的示例:

let demo =
   let swap fst snd i =
       if i = fst then snd else
       if i = snd then fst else
       i
   [ for i in 1 .. 10000 -> swap 1 i i ]

let demo2 =
   let swap (fst: int) snd i =
       if i = fst then snd else
       if i = snd then fst else
       i
   [ for i in 1 .. 10000 -> swap 1 i i ] 

两个代码块之间的唯一区别是,在第二种情况下,我将 swap 的参数显式声明为整数。然而,当我使用#time 在 fsi 中运行 2 个片段时,我得到:

案例 1 真实:00:00:00.011,CPU:00:00:00.000,GC gen0:0,gen1:0,gen2:0
案例 2 真实:00:00:00.004,CPU:00:00:00.015,GC gen0 :0,第一代:0,第二代:0

即第二个片段的运行速度是第一个片段的 3 倍。这里的绝对性能差异显然不是问题,但是如果我大量使用交换功能,它就会堆积起来。

我的假设是性能下降的原因是,在第一种情况下,swap 是通用的并且“需要相等”,并检查 int 是否支持它,而第二种情况不需要检查任何东西。这是发生这种情况的原因,还是我错过了其他东西?更一般地说,我是否应该将自动泛化视为一把双刃剑,也就是说,一个很棒的功能可能会对性能产生意想不到的影响?

4

2 回答 2

11

我认为这通常与为什么这个 F# 代码这么慢这个问题中的情况相同。在那个问题中,性能问题是由要求的约束引起的comparison,在您的情况下,它是由equality约束引起的。

在这两种情况下,编译的通用代码都必须使用接口(和装箱),而专门的编译代码可以直接使用 IL 指令进行整数或浮点数的比较或相等。

避免性能问题的两种方法是:

  • 专门化要使用的代码intfloat像您所做的那样
  • 将函数标记为inline以便编译器自动对其进行专门化

对于较小的函数,第二种方法更好,因为它不会生成太多代码,并且您仍然可以以通用方式编写函数。如果您仅将函数用于单一类型(按设计),那么使用第一种方法可能是合适的。

于 2012-05-06T23:24:01.327 回答
4

差异的原因是编译器正在生成对

    IL_0002:  call bool class [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives/HashCompare::GenericEqualityIntrinsic<!!0> (!!0, !!0)

在通用版本中,而int版本可以直接比较。

如果你使用inline我怀疑这个问题会消失,因为编译器现在有额外的类型信息

于 2012-05-06T23:25:01.550 回答