fsharp
fsharp copied to clipboard
Degraded IL codegen with .NET 9 preview 7
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)
- Download the .NET 9 preview6 and preview7 SDK zips from https://dotnet.microsoft.com/en-us/download/dotnet/9.0
- 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
- Compare
p6\dotnet.exe run -c Release
andp7\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.