.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)); ()