Skip to content

[IL Emit] Improve il code emitting when working with loops #12138

@En3Tho

Description

@En3Tho

I'm not sure whether this might be F# imporvement or JIT.. Basically I will duplicate this in runtime repo (dotnet/runtime#58941) too.

Consider these 2 methods:

[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
let fold initial folder (enumerator: #IEnumerator<'i>) =
    let folder = OptimizedClosures.FSharpFunc<_,_,_>.Adapt folder
    let mutable enumerator = enumerator
    let mutable result = initial
    while enumerator.MoveNext() do
        result <- folder.Invoke(result, enumerator.Current)
    result
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult Fold<TResult, TItem, TEnumerator>(TResult result, FSharpFunc<TResult, FSharpFunc<TItem, TResult>> folder, TEnumerator enumerator)
            where TEnumerator : IEnumerator<TItem>
{
    var fSharpFunc = OptimizedClosures.FSharpFunc<TResult, TItem, TResult>.Adapt(folder);
    var enumerator2 = enumerator;
    var result2 = result;
    while (enumerator2.MoveNext())
        result2 = fSharpFunc.Invoke(result2, enumerator2.Current);

    return result2;
}

They look very similar but there is an importnat il emit difference:

C# method is compiled to this basically:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult FoldRoslynVersion<TResult, TItem, TEnumerator>(TResult result, FSharpFunc<TResult, FSharpFunc<TItem, TResult>> folder, TEnumerator enumerator)
            where TEnumerator : IEnumerator<TItem>
{
    var fSharpFunc = OptimizedClosures.FSharpFunc<TResult, TItem, TResult>.Adapt(folder);
    var enumerator2 = enumerator;
    var result2 = result;
    goto movenext;

    logic:
    result2 = fSharpFunc.Invoke(result2, enumerator2.Current);

    movenext:
    if (!enumerator2.MoveNext())
        return result2;

    goto logic;
}

While F# is compiled to this basically:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TResult FoldFSharpVersion<TResult, TItem, TEnumerator>(TResult result, FSharpFunc<TResult, FSharpFunc<TItem, TResult>> folder, TEnumerator enumerator)
            where TEnumerator : IEnumerator<TItem>
{
    var fSharpFunc = OptimizedClosures.FSharpFunc<TResult, TItem, TResult>.Adapt(folder);
    var enumerator2 = enumerator;
    var result2 = result;

    movenext:
    if (!enumerator2.MoveNext())
        goto exit;

    result2 = fSharpFunc.Invoke(result2, enumerator2.Current);
    goto movenext;

    exit:
    return result2;
}

While difference might be non obvious, C# version with condition at the end of the method results in 10-15% perf imporvement while having the same assembly size.

image

Can F# compiler emit better IL here?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions