Почему компилятор 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 ms для iter и 61 ms для ручных проверок. Позвольте вникать в ILSpy. Соответствующая функция, вызываемая для прямого вызова:

internal static void [email protected](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);
        }
    }
}

С add inlined. Соответствующей функцией для iter является

internal static void [email protected](Microsoft.FSharp.Core.Unit unitVar0)
{
    for (int i = 0; i < 100000000; i++)
    {
        [email protected](i);
    }
}
internal static void [email protected](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

Чтобы быть понятным, компилятор F # вставляет каждое определение, которое вы отметили как inline. Это просто, что текущее поведение inlining не очень полезно при использовании встроенной функции в качестве аргумента более высокого порядка. check может быть только встроен при задании аргумента, и поэтому iter runs check рассматривается как iter runs (fun i -> check i). Затем check встраивается, что приводит к эквиваленту

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

(как вы можете видеть в IL, в сгенерированном IL нет вызова check, но есть вызов синтетического тела [email protected] для этой лямбда, что эквивалентно). iter тоже вставляется.

Сказав это, я согласен с тем, что текущее поведение не так полезно, как могло бы быть - компилятор мог также встроить тело лямбда в сайт вызова, что было бы безопасным и улучшало бы производительность.