Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Degraded IL codegen with .NET 9 preview 7 #17607

Open
jakobbotsch opened this issue Aug 26, 2024 · 1 comment · May be fixed by #17613
Open

Degraded IL codegen with .NET 9 preview 7 #17607

jakobbotsch opened this issue Aug 26, 2024 · 1 comment · May be fixed by #17613
Assignees
Labels
Area-Compiler-Optimization The F# optimizer, release code gen etc. Bug Impact-Medium (Internal MS Team use only) Describes an issue with moderate impact on existing code.

Comments

@jakobbotsch
Copy link
Member

jakobbotsch commented Aug 26, 2024

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: dotnet/runtime#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.

@vzarytovskii
Copy link
Member

vzarytovskii commented Aug 26, 2024

This is --realsig+, turning it off resolves the problem. cc @KevinRansom

@abonie abonie added Impact-Medium (Internal MS Team use only) Describes an issue with moderate impact on existing code. and removed Needs-Triage labels Aug 26, 2024
@KevinRansom KevinRansom linked a pull request Aug 26, 2024 that will close this issue
@T-Gro T-Gro removed their assignment Sep 3, 2024
@KevinRansom KevinRansom self-assigned this Sep 5, 2024
@KevinRansom KevinRansom modified the milestones: Backlog, September-2024 Sep 5, 2024
@vzarytovskii vzarytovskii modified the milestone: September-2024 Sep 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compiler-Optimization The F# optimizer, release code gen etc. Bug Impact-Medium (Internal MS Team use only) Describes an issue with moderate impact on existing code.
Projects
Status: New
Development

Successfully merging a pull request may close this issue.

6 participants
@vzarytovskii @KevinRansom @jakobbotsch @abonie @T-Gro and others