.net 通用时的性能惩罚。列表<;T>;。Add是函数中的最后一条语句,tailcall optimization处于启用状态

.net 通用时的性能惩罚。列表<;T>;。Add是函数中的最后一条语句,tailcall optimization处于启用状态,.net,f#,cil,.net,F#,Cil,我遇到了一个奇怪的性能损失,可以归结为以下代码: [<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

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

[<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
由于
List.Add
的类型是
T->unit
,我希望
Add
add1
的行为应该相同

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

i、 e.没有“尾声”。因此,当我关闭尾部调用优化时,
add
add1
以相同的速度运行

为什么
tail.
指令会导致函数调用慢得多?另外,这是一个bug还是一个特性


编辑:这是我注意到的原始代码。当末尾的
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中的bug

代码

let add j = positions.Add (Vector3(j, j, j))
大致表示“调用
列表。从值
向量3(j,j,j)上的tailcall位置添加
”,同时

表示“调用
列表。在值
向量3(j,j,j)
上添加
,然后返回
单位


就类型而言,与
List没有区别。Add
返回
unit
,因此我错误地假设了
位置。Add
将被调用,然后
Add
将返回值
unit
,这是
List.Add的返回值。但是,正如在中所述,当尾部调用函数的参数非常重要时,JIT需要执行一些“堆栈魔术”。这就是性能差距的来源。差异非常细微,但确实存在。

有趣的是,它只出现在.NET 4+中,在x86上或使用
列表中的其他数据类型时,差异要小得多。
@DaxFohl是的,我注意到x86上的差异也较低。但是我需要我的代码是64位的,所以这就是为什么我要包含这些数据。你是在RyuJIT上运行的还是在常规的旧时间JIT上运行的?您链接的文章似乎与旧文章有关。@FyodorSoikin旧文章。它很可能在新的JIT中被修复。
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)
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
let add j = positions.Add (Vector3(j, j, j))
let add1 j = positions.Add (Vector3(j, j, j)); ()