如何消除 JIT_TailCall 中对于真正非递归函数的时间消耗

7
我正在编写一个64位F#解决方案,分析表明在JIT_TailCall中花费了出乎意料的大量时间...实际上它占据了运行时间的80%左右。这与其邪恶的表兄JIT_TailCallHelperStub_ReturnAddress一起出现。
我肯定已经追踪到问题源头,即在跨越程序集边界时传递结构类型(自定义值类型)的方法或属性调用。我确定这一点,因为如果我绕过方法调用并直接将我的结构赋值给该方法使用的属性,则性能会神奇地提高4-5倍的运行时间!
调用程序集正在使用F# 3.1,因为它正在使用最新稳定版本的FSharp.Compiler.Services进行动态编译。
被调用的程序集正在使用F# 4.0 / .NET 4.6(VS 2015)。
更新:
我试图做的事情的简化是从动态生成的程序集中将自定义结构值分配给数组中的位置...
当调用以下内容时,运行时速度很快,不会生成多余的尾调用:
1.公开类型中私有数组的属性
但是,由于调用以下内容而生成多余的尾调用,导致运行时变慢:
1.公开数组的索引器属性(Item)
2.作为数组setter的成员方法
我需要调用成员方法的原因是我需要在将项插入数组之前执行一些检查。
实用:
除了理解问题的根源外,我想知道F# 4.0是否会解决这个问题,从而暗示即将发布的FSharp.Compiler.Services也会解决这个问题。考虑到更新的FSharp.Compiler.Services相对即将推出,最好等待它的到来。

我所了解的64位JItter是它不能生成非常优化的代码。这就是为什么.NET 4.6专注于RyuJIT的原因。你能否尝试使用.NET 4.6(VS 2015)并查看您看到的性能问题是否减少? - Ganesh R.
客户端程序集是在.NET 4.6和F# 4.0上编译的,但我使用最新稳定版本的FSharp.Compiler.Services生成了一个基于F# 3.1的“服务器”程序集。 - Sam
请注意,目前在CTP形式下发布的RyuJIT通常会生成更差的代码。不过我还没有测试递归。 - usr
当您完全禁用F#编译器中的尾调用时,您会看到什么性能?应用程序是否会显着加快?因为可能情况是分析器显示时间花费在尾调用上,但实际上包括一些必须完成的工作,只是在日志中以不同的方式呈现... - Tomas Petricek
@TomasPetricek [请参见问题中的更新信息] 我在除了由FSharp.Compiler.Services动态生成的程序集之外的所有程序集中禁用了尾调用。这确实显著提高了性能,但并没有达到我基于其他基准测试所期望的程度,也没有达到直接设置属性而不是通过方法传递结构体的情况下的程度。 - Sam
我有一个情况,即相互递归函数为JIT_TailCall生成30%的负载,并且为JIT_TailCallHelperStub_ReturnAddress生成15%的负载。这些函数针对方法变量和类字段进行封闭。当我关闭尾调用生成时,我的性能确切增加了45%。感谢建议将其关闭。开始将递归重写为循环... - V.B.
1个回答

1
我在您的GitHub问题上发布了这个帖子,但是在这里转发它,以便更容易找到:
我有一个情况,相互递归函数会为JIT_TailCall生成30%的负载,为JIT_TailCallHelperStub_ReturnAddress生成15%的负载。这些函数封闭了方法变量和类字段。当我关闭尾调用生成时,我的性能正好提高了45%。
我还没有对这段代码进行分析,但这就是我的真实代码结构:
#time "on"
type MyRecType() = 

  let list = System.Collections.Generic.List()

  member this.DoWork() =
    let mutable tcs = (System.Runtime.CompilerServices.AsyncTaskMethodBuilder<int>.Create())
    let returnTask = tcs.Task // NB! must access this property first
    let mutable local = 1

    let rec outerLoop() =
      if local < 1000000 then
        innerLoop(1)
      else
        tcs.SetResult(local)
        ()

    and innerLoop(inc:int) =
      if local % 2 = 0 then
        local <- local + inc
        outerLoop()
      else
        list.Add(local) // just fake access to a field to illustrate the pattern
        local <- local + 1
        innerLoop(inc)

    outerLoop()

    returnTask


let instance = MyRecType()

instance.DoWork().Result

> Real: 00:00:00.019, CPU: 00:00:00.031, GC gen0: 0, gen1: 0, gen2: 0
> val it : int = 1000001

.NET 4.6和F# 4.0没有帮助。

我尝试将其重写为方法,但出现了StackOverflowException。然而,我不明白为什么在没有尾调用生成的情况下运行大量迭代时没有导致SO?

更新 将该方法重写为:

  member this.DoWork2() =
    let mutable tcs = (System.Runtime.CompilerServices.AsyncTaskMethodBuilder<int>.Create())
    let returnTask = tcs.Task // NB! must access this property first
    let mutable local = 1
    let rec loop(isOuter:bool, inc:int) =
      if isOuter then
        if local < 1000000 then
          loop(false,1)
        else
          tcs.SetResult(local)
          ()
      else
        if local % 2 = 0 then
          local <- local + inc
          loop(true,1)
        else
          list.Add(local) // just fake access to a field to illustrate the pattern
          local <- local + 1
          loop(false,1)

    loop(true,1)

    returnTask


> Real: 00:00:00.004, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0
> val it : int = 1000001

将 JIT_TailCall 和 JIT_TailCallHelperStub_ReturnAddress 的开销减少到执行时间的18%和2%,快了2倍,因此实际开销从初始时间的45%降至10%。仍然很高,但不像第一种情况那么糟糕。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接