A promoted struct local copied from a beforefieldinit static struct field inside a loop gets its field loads hoisted into the preheader, ahead of the class-init helper, so the loads see zero-initialized static storage.
Minimal Repro
using System;
using System.Runtime.CompilerServices;
public struct S
{
public int A;
public int B;
public S(int a, int b) { A = a; B = b; }
}
public class Holder
{
public static S Value = new S(10, 20); // beforefieldinit
}
public class Program
{
public static void Main()
{
Console.WriteLine(Test(new[] { false, true, true }));
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static int Test(bool[] cond)
{
int sum = 0;
for (int i = 0; i < cond.Length; i++)
{
if (cond[i])
sum++;
S s = Holder.Value;
sum += s.A + s.B;
}
return sum;
}
}
Expected
Actual
Regression?
Not a regression: wrong result on .NET 8, 9, 10, 11 RC2 and main.
Notes
MorphCopyBlockHelper::CopyFieldByField (morphblock.cpp) creates per-field INDs via getCopyIndirFlags, which drops the source's GTF_IND_INITCLASS (and volatile/unaligned flags). Loop hoisting then doesn't treat them as cctor-dependent.
DOTNET_JitNoHoist=1 gives 92. Not reproducible with an explicit static constructor.
A promoted struct local copied from a
beforefieldinitstatic struct field inside a loop gets its field loads hoisted into the preheader, ahead of the class-init helper, so the loads see zero-initialized static storage.Minimal Repro
Expected
Actual
Regression?
Not a regression: wrong result on .NET 8, 9, 10, 11 RC2 and main.
Notes
MorphCopyBlockHelper::CopyFieldByField(morphblock.cpp) creates per-fieldINDs viagetCopyIndirFlags, which drops the source'sGTF_IND_INITCLASS(and volatile/unaligned flags). Loop hoisting then doesn't treat them as cctor-dependent.DOTNET_JitNoHoist=1gives92. Not reproducible with an explicit static constructor.