Skip to content

Conversation

@EgorBo
Copy link
Member

@EgorBo EgorBo commented Sep 12, 2025

I noticed that sometimes developers make assumptions that it's fine to use Unsafe.AsPointer over RVA fields since they're not movable, which is fairly unreliable thing to do (e.g. Unloadable ALCs). So let's just optimize the pinning overhead in JIT when we know it's safe to do so:

// Case 0: RVA fields
void Test0()
{
    ReadOnlySpan<int> rva = [1,2,3,4,5];
    fixed (int* p = rva)
        Consume(p);
}

// Case 1: Primitive static fields (unmanaged memory)
static int _primitiveFld;
void Test1()
{
    fixed (int* p = &_primitiveFld)
        Consume(p);
}


// Case 2: Struct static fields (boxed and stored on the NonGC heap)
static MyStruct _structFld;
void Test2()
{
    fixed (int* p = &_structFld.Y)
        Consume(p);
}

Codegen diff: https://www.diffchecker.com/LgIALnAn/

@github-actions github-actions bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Sep 12, 2025
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@EgorBo
Copy link
Member Author

EgorBo commented Sep 13, 2025

@MihuBot

private bool canOmitPinning(CORINFO_FIELD_STRUCT_* fldHnd)
{
FieldDesc field = HandleToObject(fldHnd);
if (!field.IsStatic || field.IsThreadStatic || field.HasGCStaticBase || field.OwningType.IsCanonicalSubtype(CanonicalFormKind.Any))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HasGCStaticBase condition should not be needed for NativeAOT.

See dotnet/runtimelab#870

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks. Removed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HasGCStaticBase condition is needed for R2R to be functionally correct. Are we seeing any crashes in GCStress+R2R with this change? If not, we may consider adding some targeted tests to validate that this optimization is valid.

It is an interesting question how conservative the contract should be for R2R. I guess we can start with the condition you had here originally. It will be one more reason why R2R is incompatible and ignored for collectible ALCs.

@EgorBo EgorBo marked this pull request as ready for review September 30, 2025 22:27
Copilot AI review requested due to automatic review settings September 30, 2025 22:27
@EgorBo
Copy link
Member Author

EgorBo commented Sep 30, 2025

@jkotas could you please review the VM side? I guess I have a few conservative checks like ThreadStatics, but I was mostly focusing on RVA fields and structs (boxed statics)
@dotnet/jit-contrib PTAL jit side

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR optimizes pinning operations in the JIT compiler by introducing a new canOmitPinning API that allows the JIT to skip pinning overhead when working with static fields that are guaranteed to be stable in memory. The optimization applies to RVA fields, primitive static fields in unmanaged memory, and struct static fields stored on the NonGC heap.

Key changes:

  • Introduces canOmitPinning JIT interface method to determine when pinning can be safely omitted
  • Enhances IsNotGcDef to consider field sequences and call the new API for static fields
  • Updates SuperPMI infrastructure to support the new interface method

Reviewed Changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/coreclr/vm/jitinterface.cpp Implements canOmitPinning with logic to check if static fields are safe to use without pinning
src/coreclr/jit/gentree.cpp Refactors IsNotGcDef to accept compiler parameter and check static field sequences
src/coreclr/jit/gentree.h Changes IsNotGcDef signature to require compiler parameter
src/coreclr/jit/lclvars.cpp Updates call to IsNotGcDef with compiler parameter
src/coreclr/inc/corinfo.h Adds canOmitPinning method declaration to JIT interface
src/coreclr/inc/jiteeversionguid.h Updates JIT-EE version identifier for interface change
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Implements simplified AOT version of canOmitPinning
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs Auto-generated code for new interface method
Various SuperPMI files Adds infrastructure support for recording and replaying canOmitPinning calls

@EgorBo EgorBo requested a review from jkotas September 30, 2025 22:27
}

return false;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was moved from gentree.h and canOmitPinning is added.
Also I added gtPeelOffsets check

TypeHandle fieldOwnerType(field->GetEnclosingMethodTable());
if (!fieldOwnerType.GetLoaderAllocator()->CanUnload() && !fieldOwnerType.IsCanonicalSubtype())
{
if (field->IsRVA())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (field->IsRVA())
if (field->IsRVA() || !field->IsByValue())

This can mirror the logic from CEEInfo::getFieldInfo

else
{
GCX_COOP();
void* pFieldAddr = field->GetCurrentStaticAddress();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void* pFieldAddr = field->GetCurrentStaticAddress();
void* pAddr = field->GetStaticAddressHandle((void*)field->GetBase());
Object* frozenObj = VolatileLoad((Object**)pAddr);
// ContainsGCPointers here is unnecessary but it's cheaper than IsInFrozenSegment
// for structs containing gc handles
if (!frozenObj->GetMethodTable()->ContainsGCPointers() &&
GCHeapUtilities::GetGCHeap()->IsInFrozenSegment(frozenObj))
{
result = true;
}

It is questionable to cast an interior pointer to Object* and pass it to the GC. It may happen to work currently but it does not sound like a good idea.

Instead, we should mirror the logic from getFieldInfo that does not have this problem,

private bool canOmitPinning(CORINFO_FIELD_STRUCT_* fldHnd)
{
FieldDesc field = HandleToObject(fldHnd);
if (!field.IsStatic || field.IsThreadStatic || field.HasGCStaticBase || field.OwningType.IsCanonicalSubtype(CanonicalFormKind.Any))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HasGCStaticBase condition is needed for R2R to be functionally correct. Are we seeing any crashes in GCStress+R2R with this change? If not, we may consider adding some targeted tests to validate that this optimization is valid.

It is an interesting question how conservative the contract should be for R2R. I guess we can start with the condition you had here originally. It will be one more reason why R2R is incompatible and ignored for collectible ALCs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI reduce-unsafe

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants