Description
Description
A method is created with System.Reflection.Emit
to be nearly equivalent to the C# code:
float Method(Func<double> f) => Math.Max(0.0f, (float) f());
The only difference being that the created method does not contain the explicit conv.r4
corresponding to the (float)
coercion.
With option <TieredCompilation>true</TieredCompilation>
the call Method(() => 2.0)
returns 2.0f
.
With option <TieredCompilation>false</TieredCompilation>
the same call returns 0.0f
.
We also observed a return value of 0.0f
with <TieredCompilation>true</TieredCompilation>
in a long-running program, so this may be dependent on the compilation tier, but I could not reduce it to a minimal example.
Reproduction Steps
using System;
using System.Reflection;
using System.Reflection.Emit;
namespace NoConvR4;
public static class Program
{
private static Type CreateType(bool dumpIL)
{
var am = new AssemblyName { Name = "MaxSqrt" };
var ab = AssemblyBuilder.DefineDynamicAssembly(am, AssemblyBuilderAccess.Run);
var mb = ab.DefineDynamicModule("TheModule");
var tb = mb.DefineType("TheType", TypeAttributes.Public);
var tfun = typeof(Func<double>);
var invoke = tfun.GetMethod("Invoke")!;
var max = typeof(Math).GetMethod(
"Max",
BindingFlags.Public | BindingFlags.Static,
[typeof(float), typeof(float)])!;
var metb = tb.DefineMethod(
"Run",
MethodAttributes.Public | MethodAttributes.Static,
typeof(float),
[tfun]);
ILGenerator il = metb.GetILGenerator();
il.Emit(OpCodes.Ldc_R4, 0.0f);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, invoke);
il.Emit(OpCodes.Call, max);
il.Emit(OpCodes.Ret);
var t = tb.CreateType();
if (dumpIL)
{
new Lokad.ILPack.AssemblyGenerator().GenerateAssembly(ab, "MaxSqrt.dll");
}
return t;
}
public static void Main(string[] args)
{
var tt = CreateType(dumpIL: false);
var method = tt.GetMethod("Run", BindingFlags.Public | BindingFlags.Static)!;
if (method.Invoke(null, new object[] { () => 2.0 }) is float f)
{
// prints 2 with tiered compilation, 0 without
Console.WriteLine($"{f}");
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<TieredCompilation>true</TieredCompilation>
</PropertyGroup>
<ItemGroup>
<!-- Used to dump the assembly, removing it does not change observed behavior -->
<PackageReference Include="Lokad.ILPack" Version="0.2.0" />
</ItemGroup>
</Project>
Expected behavior
My interpretation of ECMA-335 is that this should return 2.0f in all cases. More specifically, the implicit starg
to pass the float64 value to Math.Max(float, float)
should coerce the value to float32.
In terms of generated machine code, I would expect a vcvtsd2ss
to be produced before the vmaxss
instruction.
Actual behavior
When tiered compilation is disabled, the function returns 0.0f. The disassembly of the function shows that no vcvtsd2ss
was emitted, and so the vmaxss
interprets xmm0
to contain a float32 when it actually contains a float64.
00007FFA67EA8628 mov eax,ecx
00007FFA67EA862A mov rcx,qword ptr [rax+8]
00007FFA67EA862E call qword ptr [rax+18h]
00007FFA67EA8631 vxorps xmm1,xmm1,xmm1
00007FFA67EA8635 vmaxss xmm1,xmm1,xmm0
00007FFA67EA8639 vfixupimmss xmm1,xmm0,dword ptr [TheType.Run(System.Func`1<Double>)+030h (07FFA67EA8650h)],0
00007FFA67EA8644 vmovaps xmm0,xmm1
00007FFA67EA8648 add rsp,28h
00007FFA67EA864C ret
Regression?
This behavior was not present on .NET 6, and was observed as part of our migration from .NET 6 to .NET 8.
Known Workarounds
We can generate the additional conv.r4
to force the coercion.
Configuration
Observed on Windows 10 with runtime Microsoft.NETCore.App 8.0.0
, processor Intel i7-11850H
Observed on Ubuntu 20.04.6 LTS with runtime Microsoft.NETCore.App 8.0.2
, processor AMD EPYC 7551
Other information
No response