为什么F#编译器不能完全内联函数的高阶函数参数?

Arb*_*bil 12 performance f#

我喜欢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
Run Code Online (Sandbox Code Playgroud)

结果是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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

随着add内联.相关功能iter

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我们可以看到性能损失来自一个额外的间接层.正如性能测试所示,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
Run Code Online (Sandbox Code Playgroud)

kvb*_*kvb 6

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

iter runs (fun i -> if (add i) = 0 then printfn "")
Run Code Online (Sandbox Code Playgroud)

(正如你在IL中看到的那样,check在生成的IL中没有调用,但是f@315-5对于这个lambda 有一个对合成体的调用,这是等效的). iter也被内联了.

话虽如此,我同意当前的行为并没有那么有用 - 编译器也可以将lambda的主体内联到调用站点,这样可以安全并提高性能.

  • 严格地说,问题不是非内联的`check`,而是来自`check`的非内联函数.然而,就我所知,这并不是我的例子所特有的,而是发生在所有高阶函数调用中.因此,性能方面与未内联的函数参数相同.为什么我们(那些对使用F#的高性能/科学/游戏开发感兴趣的人)推动解决其中一些问题?1号是结构元组,这不是.2.目前在fslang上,与表现相关的提案很少. (4认同)