【问题标题】:Why can't the F# compiler fully inline function arguments of a higher order function?为什么 F# 编译器不能完全内联高阶函数的函数参数?
【发布时间】:2014-07-07 23:14:34
【问题描述】:

我喜欢 F# 的一个原因是真正的 inline 关键字。然而,虽然它允许编写执行与粘贴代码块相同的一阶函数,但对于高阶函数来说,情况并不那么乐观。考虑

let inline add i = i+1
let inline check i = if (add i) = 0 then printfn ""    
let inline iter runs f = for i = 0 to runs-1 do f i
let runs = 100000000
time(fun()->iter runs check) 1
time(fun()->for i = 0 to runs-1 do check i) 1

244 ms 的结果是 iter61 ms 的手动检查。让我们深入研究 ILSpy。直接调用调用的相关函数是:

internal static void func@22-12(Microsoft.FSharp.Core.Unit unitVar0)
{
    for (int i = 0; i < 100000000; i++)
    {
        if (i + 1 == 0)
        {
            Microsoft.FSharp.Core.PrintfFormat<Microsoft.FSharp.Core.Unit, System.IO.TextWriter, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit> format = new Microsoft.FSharp.Core.PrintfFormat<Microsoft.FSharp.Core.Unit, System.IO.TextWriter, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit>("");
            Microsoft.FSharp.Core.PrintfModule.PrintFormatLineToTextWriter<Microsoft.FSharp.Core.Unit>(System.Console.Out, format);
        }
    }
}

内联additer 的相关函数是

internal static void func@22-11(Microsoft.FSharp.Core.Unit unitVar0)
{
    for (int i = 0; i < 100000000; i++)
    {
        Tests.FunctionInlining.f@315-5(i);
    }
}
internal static void f@315-5(int i)
{
    if (i + 1 == 0)
    {
        Microsoft.FSharp.Core.PrintfFormat<Microsoft.FSharp.Core.Unit, System.IO.TextWriter, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit> format = new Microsoft.FSharp.Core.PrintfFormat<Microsoft.FSharp.Core.Unit, System.IO.TextWriter, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit>("");
        Microsoft.FSharp.Core.PrintfModule.PrintFormatLineToTextWriter<Microsoft.FSharp.Core.Unit>(System.Console.Out, format);
        return;
    }
}

我们可以看到性能损失来自一个额外的间接级别。正如性能测试所示,JIT 编译器也不会删除此间接。为什么不能完全内联高阶函数?这在编写计算内核时很痛苦。

我的时间组合器(虽然在这里并不真正相关)是

let inline time func n =
    func() |> ignore
    GC.Collect()
    GC.WaitForPendingFinalizers()
    let stopwatch = Stopwatch.StartNew()
    for i = 0 to n-1 do func() |> ignore
    stopwatch.Stop()
    printfn "Took %A ms" stopwatch.Elapsed.TotalMilliseconds

【问题讨论】:

  • 请确认您在没有附加调试器的情况下以发布模式运行此程序。除此之外,基准似乎是有效的。您可以通过将工作量增加 10 倍来消除一次性成本的影响来改进它。
  • @usr 是的,我在没有调试器的情况下运行它并在发布模式下编译。毫无疑问,性能差异是真实存在的,因为它可以从 IL 代码中推断出来(除非 JIT 优化)。
  • @Arbil 我已在有关内联分析的 F# 语言设计 UserVoice 线程之一上链接到此问题:fslang.uservoice.com/forums/245727-f-language/suggestions/…
  • 这里是关于内联优化的另一个讨论:github.com/fsharp/fsharp/issues/162
  • @JackP。谢谢。据我了解,fslang 提案更先进——它是关于算法的启发式分析。这里没有算法上的微妙之处——如果我将二阶函数和它的一阶参数函数都标记为内联,我希望应用程序的结果“一直向下”内联。这对编译器来说是一个明确的指令。我会在 fslang 中提到这一点。

标签: performance f#


【解决方案1】:

为了清楚起见,F# 编译器会内联您标记为 inline 的每个定义。只是当使用内联函数作为高阶参数时,内联的当前行为不是很有用。 check 只能在给定参数时内联,因此 iter runs check 被视为 iter runs (fun i -&gt; check i)。然后check 被内联,相当于

iter runs (fun i -> if (add i) = 0 then printfn "")

(正如您在 IL 中看到的,在生成的 IL 中没有对 check 的调用,但对这个 lambda 的合成 f@315-5 主体进行了调用,这是等效的)。 iter 也被内联。

话虽如此,我同意当前的行为并不像它应该的那样有用 - 编译器还可以将 lambda 的主体内联到调用站点,这将是安全的并提高性能。

【讨论】:

  • 严格来说确实问题不是非内联check,而是从check派生的非内联函数。然而,据我所知,这并不是我的示例所特有的,而是发生在所有高阶函数调用中。因此,在性能方面,它与未内联的函数参数相同。为什么我们(那些对使用 F# 进行高性能/科学/游戏开发感兴趣的人)不推动修复其中的一些问题? No. 1 是结构元组,这不是。 2. 目前在 fslang 上,与性能相关的提案很少。
  • 我很高兴能和你一起进行一场表演投票。高级语言令人惊叹,但“性能无关紧要”的态度导致了一个反良性循环,以至于它确实很重要。
猜你喜欢
  • 2011-01-25
  • 1970-01-01
  • 2023-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-22
  • 1970-01-01
相关资源
最近更新 更多