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

时间:2014-07-05 18:42:19

标签: 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

244 msiter的结果为61 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

1 个答案:

答案 0 :(得分:6)

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

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

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

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