Eager type init caused by P/Invoke binding dynamics #122605
-
|
Consider static types A with a method static class A
{
static bool IsInit;
static int LoopCount;
public static void OhGodWhy()
{
if (!IsInit)
return;
for (int i = 0; i < LoopCount; i++)
B.DoWork();
}
}
static class B
{
static B()
{
// observable side effect
}
public static void DoWork() { /* Actual code */ }
}(Note that this isn't a minimal repro as I couldn't isolate the behaviour.) Is this intended behaviour? And if so then what's causing it? 👀 I would love to learn more about how the JIT reasons, thank you for your time! 🙂 |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 8 replies
-
|
In practice, a type is often considered touched when a method referencing the type get touched. The timing of static constructor is non-deterministic. It's not suggested to depend on its side effect. |
Beta Was this translation helpful? Give feedback.
-
|
Is class
There is also this comment in the JIT explaining that when a type is marked You can see in the source JIT source several places where the presence of |
Beta Was this translation helpful? Give feedback.
-
|
So i initially missed reading this line:
Which is a game changer, with this information I was able to create a minimal repro and realise it has nothing to do with loop hoisting optimisations. The following code has this exact behaviour where it checks if using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
static class Program
{
static bool IsInit;
static int N = 1000;
[MethodImpl(MethodImplOptions.NoInlining)]
static unsafe void OhGodWhy()
{
if (!IsInit)
return;
byte* p = stackalloc byte[64];
for (int i = 0; i < N; i++)
B.memset((IntPtr)p, 0x7F, (UIntPtr)64);
}
static void Main()
{
Console.WriteLine("Call #1 (IsInit=false) - SHOULD NOT run B..cctor");
OhGodWhy();
Console.WriteLine("Call #2 (IsInit=true) - SHOULD run B..cctor");
IsInit = true;
OhGodWhy();
}
}
static class B
{
static B()
{
Console.WriteLine("B..cctor");
}
[SuppressGCTransition] //<- this is the culprit.
[DllImport("msvcrt.dll", EntryPoint = "memset", CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr memset(IntPtr dest, int c, UIntPtr count);
}Because this surprised me, I looked at the generated ASM around the native call site for both cases: |
Beta Was this translation helpful? Give feedback.
It sounds like related to eager bound of P/Inovke. If the P/Invoke is bound earlier, the .cctor of the type also need to be run earlier.
To be precise - there is not really a precise initialization semantic. It's only guarantee to be earlier than required point, and the implementation detail tries to be as late as possible to make it more predictable.