fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

Degraded IL codegen with .NET 9 preview 7

Open jakobbotsch opened this issue 5 months ago • 1 comments

Repro steps

We noticed one of our F# tests over in dotnet/runtime got slow enough to time out when we updated to a preview 7 SDK (issue: https://github.com/dotnet/runtime/issues/106603)

  1. Download the .NET 9 preview6 and preview7 SDK zips from https://dotnet.microsoft.com/en-us/download/dotnet/9.0
  2. Add the following to an F# project targetting .NET 9:
open System
open System.Diagnostics

// 16 byte struct
[<Struct>]
type Point2D(x: double, y: double) =
    member _.X = x
    member _.Y = y

// Will create a tail il instruction and force a tail call. This is will become
// a fast tail call on unix x64 as the caller and callee have equal stack size
let fifth() =
    let rec fifthMethodFirstCallee(iterationCount, firstArg: Point2D, secondArg: Point2D, thirdArg: Point2D, fourthArg: Point2D, fifthArg: Point2D) =
        if firstArg.X <> 10.0 then -100
        else if firstArg.Y <> 20.0 then -101
        else if secondArg.X <> 30.0 then -102
        else if secondArg.Y <> 40.0 then -103
        else if thirdArg.X <> 10.0 then -104
        else if thirdArg.Y <> 20.0 then -105
        else if fourthArg.X <> 30.0 then -106
        else if fourthArg.Y <> 40.0 then -107
        else if fifthArg.X <> 10.0 then -108
        else if fifthArg.Y <> 20.0 then -109
        else if iterationCount = 0 then
            100
        else if iterationCount % 2 = 0 then
            fifthMethodSecondCallee(iterationCount - 1, firstArg, secondArg, thirdArg, fourthArg, fifthArg)
        else
            fifthMethodFirstCallee(iterationCount - 1, firstArg, secondArg, thirdArg, fourthArg, fifthArg)

    and fifthMethodSecondCallee(iterationCount, firstArg, secondArg, thirdArg, fourthArg, fifthArg) =
        if firstArg.X <> 10.0 then -150
        else if firstArg.Y <> 20.0 then -151
        else if secondArg.X <> 30.0 then -152
        else if secondArg.Y <> 40.0 then -153
        else if thirdArg.X <> 10.0 then -154
        else if thirdArg.Y <> 20.0 then -155
        else if fourthArg.X <> 30.0 then -156
        else if fourthArg.Y <> 40.0 then -157
        else if fifthArg.X <> 10.0 then -158
        else if fifthArg.Y <> 20.0 then -159
        else if iterationCount = 0 then
            101
        else if iterationCount % 2 = 0 then
            fifthMethodSecondCallee(iterationCount - 1, firstArg, secondArg, thirdArg, fourthArg, fifthArg)
        else
            fifthMethodFirstCallee(iterationCount - 1, firstArg, secondArg, thirdArg, fourthArg, fifthArg)

    let point = Point2D(10.0, 20.0)
    let secondPoint = Point2D(30.0, 40.0)

    let retVal = fifthMethodFirstCallee(1000000, point, secondPoint, point, secondPoint, point)

    if retVal <> 100 && retVal <> 101 then
        printfn "Method -- Failed, expected result: 100 or 101, calculated: %d" retVal
        -5
    else
        0

[<EntryPoint>]
let main argv =
    let startTime = Stopwatch.StartNew()
    for i in 0..100 do
        ignore (fifth ())
    let elapsedTime = startTime.Elapsed.TotalMilliseconds
    printfn "%fms" elapsedTime
    0
  1. Compare p6\dotnet.exe run -c Release and p7\dotnet.exe run -c Release. On my machine:
❯ C:\dev\temp\sdks\p6\dotnet.exe run -c Release
330.724700ms
❯ C:\dev\temp\sdks\p7\dotnet.exe run -c Release
7513.202800ms

Looking at the C# decompilation of the method, I see the following for preview 6:

// Program
// Token: 0x06000003 RID: 3 RVA: 0x000022B4 File Offset: 0x000004B4
public static int fifth()
{
	Program.Point2D point = new Program.Point2D(10.0, 20.0);
	Program.Point2D secondPoint = new Program.Point2D(30.0, 40.0);
	int retVal = Program.fifthMethodFirstCallee@13(1000000, point, secondPoint, point, secondPoint, point);
	if (retVal != 100 && retVal != 101)
	{
		PrintfFormat<FSharpFunc<int, Unit>, TextWriter, Unit, Unit> printfFormat = new PrintfFormat<FSharpFunc<int, Unit>, TextWriter, Unit, Unit, int>("Method -- Failed, expected result: 100 or 101, calculated: %d");
		PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<int, Unit>>(Console.Out, printfFormat).Invoke(retVal);
		return -5;
	}
	return 0;
}

and the following fore preview 7:

// Program
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
public static int fifth()
{
	FSharpFunc<int, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, int>>>>>> fsharpFunc2;
	FSharpFunc<int, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, int>>>>>> fsharpFunc = new Program.fifthMethodFirstCallee@28(fsharpFunc2);
	fsharpFunc2 = new Program.fifthMethodSecondCallee@46(fsharpFunc);
	((Program.fifthMethodFirstCallee@28)fsharpFunc).fifthMethodSecondCallee@46 = fsharpFunc2;
	Program.Point2D point = new Program.Point2D(10.0, 20.0);
	Program.Point2D secondPoint = new Program.Point2D(30.0, 40.0);
	FSharpFunc<int, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, FSharpFunc<Program.Point2D, int>>>>>> fsharpFunc3 = fsharpFunc;
	int num = 1000000;
	Program.Point2D point2D = point;
	Program.Point2D point2D2 = secondPoint;
	Program.Point2D point2D3 = point;
	Program.Point2D point2D4 = secondPoint;
	Program.Point2D point2D5 = point;
	int retVal = FSharpFunc<int, Program.Point2D>.InvokeFast<Program.Point2D, Program.Point2D, Program.Point2D, FSharpFunc<Program.Point2D, int>>(fsharpFunc3, num, point2D, point2D2, point2D3, point2D4).Invoke(point2D5);
	if (retVal != 100 && retVal != 101)
	{
		PrintfFormat<FSharpFunc<int, Unit>, TextWriter, Unit, Unit> printfFormat = new PrintfFormat<FSharpFunc<int, Unit>, TextWriter, Unit, Unit, int>("Method -- Failed, expected result: 100 or 101, calculated: %d");
		PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<int, Unit>>(Console.Out, printfFormat).Invoke(retVal);
		return -5;
	}
	return 0;
}

so seems like there are some quite significant differences in the IL codegen.

Expected behavior

Equivalent performance.

Actual behavior

Performance seems degraded.

jakobbotsch avatar Aug 26 '24 13:08 jakobbotsch