Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Smaller static constructor context (#83937)
This makes native aot compiled binaries smaller and reduces code size of inlined static constructor checks that are being worked on.
  • Loading branch information
jkotas committed Mar 29, 2023
1 parent db93e03 commit 2d90e25
Show file tree
Hide file tree
Showing 17 changed files with 98 additions and 109 deletions.
Expand Up @@ -52,13 +52,13 @@ private static unsafe object CheckStaticClassConstructionReturnThreadStaticBase(
public static unsafe void EnsureClassConstructorRun(StaticClassConstructionContext* pContext)
{
IntPtr pfnCctor = pContext->cctorMethodAddress;
NoisyLog("EnsureClassConstructorRun, cctor={0}, thread={1}", pfnCctor, CurrentManagedThreadId);
NoisyLog("EnsureClassConstructorRun, context={0}, thread={1}", pContext, CurrentManagedThreadId);

// If we were called from MRT, this check is redundant but harmless. This is in case someone within classlib
// (cough, Reflection) needs to call this explicitly.
if (pContext->initialized == 1)
if (pfnCctor == 0)
{
NoisyLog("Cctor already run, cctor={0}, thread={1}", pfnCctor, CurrentManagedThreadId);
NoisyLog("Cctor already run, context={0}, thread={1}", pContext, CurrentManagedThreadId);
return;
}

Expand All @@ -68,22 +68,22 @@ public static unsafe void EnsureClassConstructorRun(StaticClassConstructionConte
try
{
Lock cctorLock = cctors[cctorIndex].Lock;
if (DeadlockAwareAcquire(cctor, pfnCctor))
if (DeadlockAwareAcquire(cctor, pContext))
{
int currentManagedThreadId = CurrentManagedThreadId;
try
{
NoisyLog("Acquired cctor lock, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);
NoisyLog("Acquired cctor lock, context={0}, thread={1}", pContext, currentManagedThreadId);

cctors[cctorIndex].HoldingThread = currentManagedThreadId;
if (pContext->initialized == 0) // Check again in case some thread raced us while we were acquiring the lock.
if (pContext->cctorMethodAddress != 0) // Check again in case some thread raced us while we were acquiring the lock.
{
TypeInitializationException priorException = cctors[cctorIndex].Exception;
if (priorException != null)
throw priorException;
try
{
NoisyLog("Calling cctor, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);
NoisyLog("Calling cctor, context={0}, thread={1}", pContext, currentManagedThreadId);

((delegate*<void>)pfnCctor)();

Expand All @@ -94,9 +94,9 @@ public static unsafe void EnsureClassConstructorRun(StaticClassConstructionConte
// still see uninitialized static fields on the class.
Interlocked.MemoryBarrier();

NoisyLog("Set type inited, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);
NoisyLog("Set type inited, context={0}, thread={1}", pContext, currentManagedThreadId);

pContext->initialized = 1;
pContext->cctorMethodAddress = 0;
}
catch (Exception e)
{
Expand All @@ -109,7 +109,7 @@ public static unsafe void EnsureClassConstructorRun(StaticClassConstructionConte
finally
{
cctors[cctorIndex].HoldingThread = ManagedThreadIdNone;
NoisyLog("Releasing cctor lock, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);
NoisyLog("Releasing cctor lock, context={0}, thread={1}", pContext, currentManagedThreadId);

cctorLock.Release();
}
Expand All @@ -124,15 +124,15 @@ public static unsafe void EnsureClassConstructorRun(StaticClassConstructionConte
{
Cctor.Release(cctor);
}
NoisyLog("EnsureClassConstructorRun complete, cctor={0}, thread={1}", pfnCctor, CurrentManagedThreadId);
NoisyLog("EnsureClassConstructorRun complete, context={0}, thread={1}", pContext, CurrentManagedThreadId);
}

//=========================================================================================================
// Return value:
// true - lock acquired.
// false - deadlock detected. Lock not acquired.
//=========================================================================================================
private static bool DeadlockAwareAcquire(CctorHandle cctor, IntPtr pfnCctor)
private static unsafe bool DeadlockAwareAcquire(CctorHandle cctor, StaticClassConstructionContext* pContext)
{
const int WaitIntervalSeedInMS = 1; // seed with 1ms and double every time through the loop
const int WaitIntervalLimitInMS = WaitIntervalSeedInMS << 7; // limit of 128ms
Expand Down Expand Up @@ -181,8 +181,8 @@ private static bool DeadlockAwareAcquire(CctorHandle cctor, IntPtr pfnCctor)
if (holdingThread == currentManagedThreadId)
{
// Deadlock detected. We will break the guarantee and return without running the .cctor.
DebugLog("A class constructor was skipped due to class constructor cycle. cctor={0}, thread={1}",
pfnCctor, currentManagedThreadId);
DebugLog("A class constructor was skipped due to class constructor cycle. context={0}, thread={1}",
pContext, currentManagedThreadId);

// We are maintaining an invariant that the BlockingRecords never show a cycle because,
// before we add a record, we first check for a cycle. As a result, once we've said
Expand Down Expand Up @@ -223,7 +223,7 @@ private static bool DeadlockAwareAcquire(CctorHandle cctor, IntPtr pfnCctor)
// respect to other updates to the BlockingRecords.
if (unmarkCookie == -1)
{
NoisyLog("Mark thread blocked, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);
NoisyLog("Mark thread blocked, context={0}, thread={1}", pContext, currentManagedThreadId);

unmarkCookie = BlockingRecord.MarkThreadAsBlocked(currentManagedThreadId, cctor);
}
Expand All @@ -241,7 +241,7 @@ private static bool DeadlockAwareAcquire(CctorHandle cctor, IntPtr pfnCctor)
{
if (unmarkCookie != -1)
{
NoisyLog("Unmark thread blocked, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);
NoisyLog("Unmark thread blocked, context={0}, thread={1}", pContext, currentManagedThreadId);
BlockingRecord.UnmarkThreadAsBlocked(unmarkCookie);
}
}
Expand Down Expand Up @@ -493,22 +493,22 @@ internal static void Initialize()
}

[Conditional("ENABLE_NOISY_CCTOR_LOG")]
private static void NoisyLog(string format, IntPtr cctorMethod, int threadId)
private static unsafe void NoisyLog(string format, StaticClassConstructionContext* pContext, int threadId)
{
// We cannot utilize any of the typical number formatting code because it triggers globalization code to run
// and this cctor code is layered below globalization.
#if DEBUG
Debug.WriteLine(format, ToHexString(cctorMethod), ToHexString(threadId));
Debug.WriteLine(format, ToHexString((IntPtr)pContext), ToHexString(threadId));
#endif // DEBUG
}

[Conditional("DEBUG")]
private static void DebugLog(string format, IntPtr cctorMethod, int threadId)
private static unsafe void DebugLog(string format, StaticClassConstructionContext* pContext, int threadId)
{
// We cannot utilize any of the typical number formatting code because it triggers globalization code to run
// and this cctor code is layered below globalization.
#if DEBUG
Debug.WriteLine(format, ToHexString(cctorMethod), ToHexString(threadId));
Debug.WriteLine(format, ToHexString((IntPtr)pContext), ToHexString(threadId));
#endif
}

Expand Down
Expand Up @@ -8,23 +8,14 @@
namespace System.Runtime.CompilerServices
{
// This structure is used to pass context about a type's static class construction state from the runtime
// to the classlibrary via the CheckStaticClassConstruction callback. The runtime knows about the first
// two fields (cctorMethodAddress and initialized) and thus these must remain the first two fields in the
// same order and at the same offset (hence the sequential layout attribute). It is permissable for the
// classlibrary to add its own fields after these for its own use however. These must not contain GC
// to the classlibrary via the CheckStaticClassConstruction callback. It is permissable for the
// classlibrary to add its own fields after these for its own use. These must not contain GC
// references and will be zero initialized.
[CLSCompliant(false)]
[StructLayout(LayoutKind.Sequential)]
public struct StaticClassConstructionContext
{
// Pointer to the code for the static class constructor method. This is initialized by the
// binder/runtime.
public IntPtr cctorMethodAddress;

// Initialization state of the class. This is initialized to 0. Every time managed code checks the
// cctor state the runtime will call the classlibrary's CheckStaticClassConstruction with this context
// structure unless initialized == 1. This check is specific to allow the classlibrary to store more
// than a binary state for each cctor if it so desires.
public volatile int initialized;
// Pointer to the code for the static class constructor method. Set to 0 once the cctor has run.
public volatile IntPtr cctorMethodAddress;
}
}
Expand Up @@ -53,7 +53,7 @@ public TypeBuilder()
/// The StaticClassConstructionContext for a type is encoded in the negative space
/// of the NonGCStatic fields of a type.
/// </summary>
public static readonly unsafe int ClassConstructorOffset = -sizeof(System.Runtime.CompilerServices.StaticClassConstructionContext);
public static unsafe int ClassConstructorOffset => -sizeof(System.Runtime.CompilerServices.StaticClassConstructionContext);

private LowLevelList<TypeDesc> _typesThatNeedTypeHandles = new LowLevelList<TypeDesc>();

Expand Down
Expand Up @@ -40,33 +40,31 @@ private static unsafe void CheckStaticClassConstruction(ref StaticClassConstruct
while (true)
{
// Read the current state of the cctor.
int oldInitializationState = context.initialized;
IntPtr oldInitializationState = context.cctorMethodAddress;

// Once it transitions to 1 then the cctor has been run (another thread got there first) and
// Once it transitions to 0 then the cctor has been run (another thread got there first) and
// we can simply return.
if (oldInitializationState == 1)
if (oldInitializationState == (IntPtr)0)
return;

// If the state is anything other than 0 (the initial state) then another thread is currently
// running this cctor. We must wait for it to complete doing so before continuing, so loop
// again.
if (oldInitializationState != 0)
// If the state is 1 then another thread is currently running this cctor.
// We must wait for it to complete doing so before continuing, so loop again.
if (oldInitializationState == (IntPtr)1)
continue;

// C# warns that passing a volatile field to a method via a reference loses the volatility of the field.
// However the method in question is Interlocked.CompareExchange so the volatility in this case is
// unimportant.
#pragma warning disable 420

// We read a state of 0 (the initial state: not initialized and not being initialized). Try to
// transition this to 2 which will let other threads know we're going to run the cctor here.
if (Interlocked.CompareExchange(ref context.initialized, 2, 0) == 0)
// Try to transition this to 1 which will let other threads know we're going to run the cctor here.
if (Interlocked.CompareExchange(ref context.cctorMethodAddress, (IntPtr)1, oldInitializationState) == oldInitializationState)
{
// We won the race to transition the state from 0 -> 2. So we can now run the cctor. Other
// We won the race to transition the state to 1. So we can now run the cctor. Other
// threads trying to do the same thing will spin waiting for us to transition the state to
// 1.

((delegate*<void>)context.cctorMethodAddress)();
((delegate*<void>)oldInitializationState)();

// Insert a memory barrier here to order any writes executed as part of static class
// construction above with respect to the initialized flag update we're about to make
Expand All @@ -75,9 +73,9 @@ private static unsafe void CheckStaticClassConstruction(ref StaticClassConstruct
// still see uninitialized static fields on the class.
Interlocked.MemoryBarrier();

// Set the state to 1 to indicate to the runtime and other threads that this cctor has now
// Set the cctorMethodAddress to 0 to indicate to the runtime and other threads that this cctor has now
// been run.
context.initialized = 1;
context.cctorMethodAddress = (IntPtr)0;
}

// If we get here some other thread changed the initialization state to a non-zero value
Expand Down
Expand Up @@ -6,22 +6,13 @@
namespace System.Runtime.CompilerServices
{
// This structure is used to pass context about a type's static class construction state from the runtime
// to the classlibrary via the CheckStaticClassConstruction callback. The runtime knows about the first
// two fields (cctorMethodAddress and initialized) and thus these must remain the first two fields in the
// same order and at the same offset (hence the sequential layout attribute). It is permissable for the
// classlibrary to add its own fields after these for its own use however. These must not contain GC
// to the classlibrary via the CheckStaticClassConstruction callback. It is permissable for the
// classlibrary to add its own fields after these for its own use. These must not contain GC
// references and will be zero initialized.
[StructLayout(LayoutKind.Sequential)]
public struct StaticClassConstructionContext
{
// Pointer to the code for the static class constructor method. This is initialized by the
// binder/runtime.
public IntPtr cctorMethodAddress;

// Initialization state of the class. This is initialized to 0. Every time managed code checks the
// cctor state the runtime will call the classlibrary's CheckStaticClassConstruction with this context
// structure unless initialized == 1. This check is specific to allow the classlibrary to store more
// than a binary state for each cctor if it so desires.
public volatile int initialized;
// Pointer to the code for the static class constructor method. Set to 0 .
public volatile IntPtr cctorMethodAddress;
}
}
Expand Up @@ -99,6 +99,10 @@ internal static unsafe Array RhNewArray(EETypePtr pEEType, int length)
[RuntimeImport(RuntimeLibrary, "RhpLockCmpXchg32")]
internal static extern int InterlockedCompareExchange(ref int location1, int value, int comparand);

[MethodImplAttribute(MethodImplOptions.InternalCall)]
[RuntimeImport(RuntimeLibrary, "RhpLockCmpXchg64")]
internal static extern long InterlockedCompareExchange(ref long location1, long value, long comparand);

[MethodImplAttribute(MethodImplOptions.InternalCall)]
[RuntimeImport(RuntimeLibrary, "RhpMemoryBarrier")]
internal static extern void MemoryBarrier();
Expand Down
Expand Up @@ -8,12 +8,28 @@ namespace System.Threading
{
public static class Interlocked
{
[Intrinsic]
public static IntPtr CompareExchange(ref IntPtr location1, IntPtr value, IntPtr comparand)
{
#if TARGET_64BIT
return (IntPtr)Interlocked.CompareExchange(ref Unsafe.As<IntPtr, long>(ref location1), (long)value, (long)comparand);
#else
return (IntPtr)Interlocked.CompareExchange(ref Unsafe.As<IntPtr, int>(ref location1), (int)value, (int)comparand);
#endif
}

[Intrinsic]
public static int CompareExchange(ref int location1, int value, int comparand)
{
return RuntimeImports.InterlockedCompareExchange(ref location1, value, comparand);
}

[Intrinsic]
public static long CompareExchange(ref long location1, long value, long comparand)
{
return RuntimeImports.InterlockedCompareExchange(ref location1, value, comparand);
}

[Intrinsic]
public static void MemoryBarrier()
{
Expand Down
Expand Up @@ -103,7 +103,7 @@ public static int GetClassConstructorContextSize(TargetDetails target)
{
// TODO: Assert that StaticClassConstructionContext type has the expected size
// (need to make it a well known type?)
return target.PointerSize * 2;
return target.PointerSize;
}

private static int GetClassConstructorContextStorageSize(TargetDetails target, MetadataType type)
Expand Down Expand Up @@ -172,26 +172,15 @@ protected override ObjectData GetDehydratableData(NodeFactory factory, bool relo
// cctor context as already executed.
if (!HasLazyStaticConstructor)
{
// Pointer to the cctor: we don't care - emit as zero
// Pointer to the cctor: Zero means initialized
builder.EmitZeroPointer();

// Constructor executed
// TODO-NICE: introduce a named constant and also use it in the runner in CoreLib
builder.EmitInt(1);
}
else
{
// Emit pointer to the cctor
MethodDesc cctorMethod = _type.GetStaticConstructor();
builder.EmitPointerReloc(factory.ExactCallableAddress(cctorMethod));

// Constructor didn't execute
builder.EmitInt(0);
}

// Emit padding if needed
if (builder.TargetPointerSize == 8)
builder.EmitInt(0);
}
else
{
Expand Down

0 comments on commit 2d90e25

Please sign in to comment.