当 Generic.List.Add 是函数中的最后一个语句并且开启尾调用优化时,性能会受到影响

2024-05-24

我遇到了一个奇怪的性能损失,我将其归结为以下代码:

[<Struct>]
type Vector3(x: float32, y: float32, z: float32) =
  member this.X = x
  member this.Y = y
  member this.Z = z

type Data(n: int) =   
  let positions = System.Collections.Generic.List<Vector3>()
  let add j = positions.Add (Vector3(j, j, j))
  let add1 j = positions.Add (Vector3(j, j, j)); ()
  member this.UseAdd () = for i = 1 to n do add (float32 i)
  member this.UseAdd1 () = for i = 1 to n do add1 (float32 i)

let timeIt name (f: unit -> unit) = 
  let timer = System.Diagnostics.Stopwatch.StartNew()
  f ()
  printfn "%s: %ims" name (int timer.ElapsedMilliseconds)

let test () =
  for i = 1 to 3 do timeIt "ADD" (fun () -> Data(1000000).UseAdd())
  for i = 1 to 3 do timeIt "ADD1" (fun () -> Data(1000000).UseAdd1())

[<EntryPoint>]
let main argv = 
  test ()
  0

和...之间的不同add and add1是额外的()在最后。

当我在 .NET 4.5.1 上使用 F# 3.1 将其构建为 x64 Release 构建时,我得到以下输出:

ADD: 461ms
ADD: 457ms
ADD: 450ms
ADD1: 25ms
ADD1: 26ms
ADD1: 16ms

由于类型为List<T>.Add is T -> unit我希望add and add1应该表现相同。

使用 ILdasm 我发现add编译为(仅包括相关部分)

IL_000a:  newobj     instance void Program/Vector3::.ctor(float32,
                                                          float32,
                                                          float32)
IL_000f:  tail.
IL_0011:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<valuetype Program/Vector3>::Add(!0)

while add1 into

IL_000a:  newobj     instance void Program/Vector3::.ctor(float32,
                                                          float32,
                                                          float32)
IL_000f:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<valuetype Program/Vector3>::Add(!0)

即没有“尾部调用”。因此,当我关闭尾部调用优化时,两者add and add1以相同的速度运行。

为什么tail.指令导致函数调用慢很多?另外,这是一个错误还是一个功能?


编辑:这是原始代码,我注意到了这种行为。当。。。的时候true最后的值被丢弃,它表现出与上面的代码相同的性能下降。

let makeAtom (ctx: CleanCifContext) (element: CleanCifAtomSiteElement) = 
  let residue = getResidue ctx element

  let position =
    Vector3(float32 (element.PositionX.ValueOrFail()), float32 (element.PositionY.ValueOrFail()), float32 (element.PositionZ.ValueOrFail()))
  let atom = 
    CifAtom(id = ctx.Atoms.Count, element = element.ElementSymbol.ValueOrFail(),
            residue = residue, serialNumber = element.Id.ValueOrFail(), 
            name = element.Name.ValueOrFail(), authName = element.AuthName.Value(), altLoc = element.AltLoc.Value(),
            occupancy = float32 (element.Occupancy.ValueOrFail()), tempFactor = float32 (element.TempFactor.ValueOrFail()))

  ctx.Atoms.Add atom
  ctx.Positions.Add position
  true

我想我已经弄清楚问题出在哪里,以及为什么这是我对问题的误解,而不是 F# 编译器或 .NET 中的错误。

代码

let add j = positions.Add (Vector3(j, j, j))

大致意思是“调用List<T>.Add从值的尾部调用位置开始Vector3(j, j, j)" while

let add1 j = positions.Add (Vector3(j, j, j)); ()

意思是“打电话List<T>.Add关于价值Vector3(j, j, j)然后返回unit".

从类型上看,没有区别List<T>.Add回报unit所以我错误地假设positions.Add会被打电话然后add将返回值unit这是的返回值List<T>.Add。然而,正如所述http://blogs.msdn.com/b/clrcode Generation/archive/2009/05/11/tail-call-improvements-in-net-framework-4.aspx http://blogs.msdn.com/b/clrcodegeneration/archive/2009/05/11/tail-call-improvements-in-net-framework-4.aspx,当尾调用函数的参数不平凡时,JIT 需要执行一些“堆栈魔术”。这就是性能差距的来源。差异非常微妙,但确实存在。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

当 Generic.List.Add 是函数中的最后一个语句并且开启尾调用优化时,性能会受到影响 的相关文章

随机推荐