Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't the F# compiler fully inline function arguments of a higher order function?

Tags:

performance

f#

One of the things I love about F# is a real inline keyword. However, while it allows to write first order functions that perform the same as pasted code blocks, things aren't so rosy for higher order functions. Consider

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

The results are 244 ms for iter and 61 ms for manual checks. Let's delve into ILSpy. The relevant function called for the direct call is:

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);
        }
    }
}

With add inlined. The relevant function for iter is

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;
    }
}

And we can see the performance penalty comes from one extra level of indirection. As the performance test shows, this indirection is also not removed by the JIT compiler. Is there a reason for why the higher order functions cannot be fully inlined? This is a pain when writing a computational kernel.

My time combinator (though not really relevant here) is

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
like image 403
Arbil Avatar asked Jul 05 '14 18:07

Arbil


People also ask

Why is the F-22 being replaced?

USAF sought to retire the early F-22s, currently rated for training use only because they are expensive to maintain and are increasingly mismatched to the combat-coded versions, reducing their value as training platforms. The roughly $1 billion cost to upgrade those jets was not affordable, Air Force officials said.

Why can't the F-35 fly in the rain?

In spring 2020, officials banned the F-35A from flying within 25 nautical miles of lightning or thunderstorms after finding that a crucial system may not function correctly if hit by a bolt. At issue is the Onboard Inert Gas Generation System, or OBIGGS, which injects nitrogen-enriched air into the jet's fuel tanks.

Is the F-22 still in service?

U.S. Air Force plans to begin retirement of F-22 Raptor fifth generation fighters from service in 2023 have been met with strong opposition from both the House and Senate Armed Services Committees, which have pressed to not only keep the small fleet at full strength but also to invest heavily in upgrades.

How many F-22s have crashed?

The F-22 fleet, which now numbers 182 aircraft, has experienced 32 “Class A” mishaps and 50 “Class B” accidents over the past 21 years. A Class A accident involves a fatality, loss of the aircraft, or more than $2.5 million in damage.


1 Answers

Just to be clear, the F# compiler is inlining every definition that you've marked as inline. It's just that the current behavior of inlining isn't very useful when using an inlined function as a higher-order argument. check can only be inlined when given an argument, and so iter runs check is treated as iter runs (fun i -> check i). Then check gets inlined, resulting in the equivalent of

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

(as you can see in the IL, there is no call to check in the generated IL, but there is a call to the synthetic f@315-5 body for this lambda, which is equivalent). iter gets inlined, too.

Having said that, I agree that the current behavior isn't as useful as it could be - the compiler could also inline the body of the lambda into the call site, which would be safe and improve performance.

like image 158
kvb Avatar answered Sep 25 '22 00:09

kvb