Skip to content

[JIT] Asm difference for F# and C# methods #58941

@En3Tho

Description

@En3Tho

I'm not sure whether this might be JIt imporvement or F#.. Basically I will duplicate this in fsharp repo (dotnet/fsharp#12138) 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 JIT compiler regonize these patterns better and ideally emit the same code for both variants?

category:cq
theme:loop-opt
skill-level:expert
cost:large
impact:large

Metadata

Metadata

Assignees

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMItenet-performancePerformance related issue

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions