Skip to content

Commit

Permalink
Make normal statics simpler (#99183)
Browse files Browse the repository at this point in the history
This change makes access to statics much simpler to document and also removes some performance penalties that we've had for a long time due to the old model. Most statics access should be equivalent or faster.

This change converts static variables from a model where statics are associated with the module that defined the metadata of the static to a model where each individual type allocates its statics independently. In addition, it moves the flags that indicate whether or not a type is initialized, and whether or not its statics have been allocated to the `MethodTable` structures instead of storing them in a `DomainLocalModule` as was done before.

# Particularly notable changes
- All statics are now considered "dynamic" statics.
- Statics for collectible assemblies now have an identical path for lookup of the static variable addresses as compared to statics for non-collectible assemblies. It is now reasonable for the process of reading static variables to be inlined into shared generic code, although this PR does not attempt to do so.
- Lifetime management for collectible non-thread local statics is managed via a combination of a `LOADERHANDLE` to keep the static alive, and a new handle type called a `HNDTYPE_WEAK_INTERIOR_POINTER` which will keep the pointers to managed objects in the `MethodTable` structures up to date with the latest addresses of the static variables.
- Each individual type in thread statics has a unique object holding the statics for the type. This means that each type has a separate object[](for gc statics), and/or double[](for non-gc statics) per thread for TLS statics. This isn't necessarily ideal for non-collectible types, but its not terrible either.
- Thread statics for collectible types are reported directly to the GC instead of being handled via a GCHandle. While needed to avoid complex lifetime rules for collectible types, this may not be ideal for non-collectable types.
- Since the `DomainLocalModule` no longer exists, the `ISOSDacInterface` has been augmented with a new api called `ISOSDacInterface14` which adds the ability to query for the static base/initialization status of an individual type directly.
- Significant changes for generated code include
  - All the helpers are renamed
  - The statics of generics which have not yet been initialized can now be referenced using a single constant pointer + a helper call instead of needing a pair of pointers. In practice, this was a rare condition in perf-critical code due to the presence of tiered compilation, so this is not a significant change to optimized code.
  - The pre-initialization of statics can now occur for types which have non-primitive valuetype statics as long as the type does not have a class constructor.
  - Thread static non-gc statics are now returned as byrefs. (It turns out that for collectible assemblies, there is currently a small GC hole if a function returns the address of a non-gc threadstatic. CoreCLR at this time does not attempt to keep the collectible assembly alive if that is the only live pointer to the collectible static in the system)

With this change, the pointers to normal static data are located at a fixed offset from the start of the `MethodTableAuxiliaryData`, and indices for Thread Static variables are stored also stored in such a fixed offset. Concepts such as the `DomainLocalModule` , `ThreadLocalModule`, `ModuleId` and `ModuleIndex` no longer exist.

# Lifetime management for collectible statics
- For normal collectible statics, each type will allocate a separate object[] for the GC statics and a double[] for the non-GC statics. A pointer to the data of these arrays will be stored in the `DynamicStaticsInfo` structure, and when relocation occurs, if the collectible types managed `LoaderAllocator` is still alive, the static field address will be relocated if the object moves. This is done by means of the new Weak Interior Pointer GC handle type. 
- For collectible thread-local statics, the lifetime management is substantially more complicated due the issue that it is possible for either a thread or a collectible type to be collected first. Thus the collection algorithm is as follows.
  - The system shall maintain a global mapping of TLS indices to MethodTable structures
  - When a native `LoaderAllocator` is being cleaned up, before the WeakTrackResurrection GCHandle that points at the the managed `LoaderAllocator` object is destroyed, the mapping from TLS indices to collectible `LoaderAllocator` structures shall be cleared of all relevant entries (and the current GC index shall be stored in the TLS to MethodTable mapping)
  - When a GC promotion or collection scan occurs, for every TLS index which was freed to point at a GC index the relevant entry in the TLS table shall be set to NULL in preparation for that entry in the table being reused in the future. In addition, if the TLS index refers to a `MethodTable` which is in a collectible assembly, and the associated `LoaderAllocator` has been freed, then set the relevant entry to NULL.
  - When allocating new entries from the TLS mapping table for new collectible thread local structures, do not re-use an entry in the table until at least 2 GCs have occurred. This is to allow every thread to have NULL'd out the relevant entry in its thread local table.
  - When allocating new TLS entries for collectible TLS statics on a per-thread basis allocate a `LOADERHANDLE` for each object allocated, and associate it with the TLS index on that thread.
  - When cleaning up a thread, for each collectible thread static which is still allocated, we will have a `LOADERHANDLE`. If the collectible type still has a live managed `LoaderAllocator` free the `LOADERHANDLE`.

# Expected cost model for extra GC interactions associated with this change
This change adds 3 possible ways in which the GC may have to perform additional work beyond what it used to do.
1. For normal statics on collectible types, it uses the a weak interior pointer GC handle for each of these that is allocated. This is purely pay for play and trades off performance of accessing collectible statics at runtime to the cost of maintaining a GCHandle in the GC. As the number of statics increases, this could in theory become a performance problem, but given the typical usages of collectible assemblies, we do not expect this to be significant.
2. For non-collectible thread statics, there is 1 GC pointer that is unconditionally reported for each thread. Usage of this removes a single indirection from every non-collectible thread local access. Given that this pointer is reported unconditionally, and is only a single pointer, this is not expected to be a significant cost.
3. For collectible thread statics, there is a complex protocol to keep thread statics alive for just long enough, and to clean them up as needed. This is expected to be completely pay for play with regard to usage of thread local variables in collectible assemblies, and while slightly more expensive to run than the current logic, will reduce the cost of creation/destruction of threads by a much more significant factor. In addition, if there are no collectible thread statics used on the thread, the cost of this is only a few branches per lookup.

# Perf impact of this change
I've run the .NET Microbenchmark suite as well as a variety of ASP.NET Benchmarks. (Unfortunately the publicly visible infrastructure for running tests is incompatible with this change, so results are not public). The results are generally quite hard to interpret. ASP.NET Benchmarks are generally (very) slightly better, and the microbenchmarks are generally equivalent in performance, although there is variability in some tests that had not previously shown variability, and the differences in performance are contained within the margin of error in our perf testing for tests with any significant amount of code. When performance differences have been examined in detail, they tend to be in code which has not changed in any way due to this change, and when run in isolation the performance deltas have disappeared in all cases that I have examined. Thus, I assume they are caching side effect changes. Performance testing has led me to add a change such that all NonGC, NonCollectible statics are allocated in a separate LoaderHeap which appears to have reduced the variability in some of the tests by a small fraction, although results are not consistent enough for me to be extremely confident in that statement.
  • Loading branch information
davidwrighton authored Jun 13, 2024
1 parent bfb028b commit eb8f54d
Show file tree
Hide file tree
Showing 106 changed files with 3,368 additions and 5,957 deletions.
151 changes: 138 additions & 13 deletions docs/design/coreclr/botr/type-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,143 @@ This is enforced via an extensive and complicated set of enforcements within the
- **ISSUE:** The signature walks performed are done with the normal signature walking code. This code is designed to load types as it walks the signature, but in this case the type load functionality is used with the assumption that no type load will actually be triggered.
- **ISSUE:** Stackwalker requirements require support from not just the type system, but also the assembly loader. The Loader has had a number of issues meeting the needs of the type system here.

Type System and NGEN
--------------------

The type system data structures are a core part of what is saved into NGEN images. Unfortunately, these data structures logically have pointers within them that point to other NGEN images. In order to handle this situation, the type system data structures implement a concept known as restoration.

In restoration, when a type system data structure is first needed, the data structure is fixed up with correct pointers. This is tied into the type loading levels described in the [Type Loader](type-loader.md) Book of the Runtime chapter.

There also exists the concept of pre-restored data structures. This means that the data structure is sufficiently correct at NGEN image load time (after intra-module pointer fixups and eager load type fixups), that the data structure may be used as is. This optimization requires that the NGEN image be "hard bound" to its dependent assemblies. See NGEN documentation for further details.

Type System and Domain Neutral Loading
--------------------------------------

The type system is a core part of the implementation of domain neutral loading. This is exposed to customers through the LoaderOptimization options available at AppDomain creation. Mscorlib is always loaded as domain neutral. The core requirement of this feature is that the type system data structures must not require pointers to domain specific state. Primarily this manifests itself in requirements around static fields and class constructors. In particular, whether or not a class constructor has been run is not a part of the core MethodTable data structure for this reason, and there is a mechanism for storing static data attached to the DomainFile data structure instead of the MethodTable data structure.
## Static variables

Static variables in CoreCLR are handled by a combination of getting the "static base", and then adjusting it by an offset to get a pointer to the actual value.
We define the statics base as either non-gc or gc for each field.
Currently non-gc statics are any statics which are represented by primitive types (byte, sbyte, char, int, uint, long, ulong, float, double, pointers of various forms), and enums.
GC statics are any statics which are represented by classes or by non-primitive valuetypes.
For valuetype statics which are GC statics, the static variable is actually a pointer to a boxed instance of the valuetype.

### Per type static variable information
As of .NET 9, the static variable bases are now all associated with their particular type.
As you can see from this diagram, the data for statics can be acquired by starting at a `MethodTable` and then getting either the `DynamicStaticsInfo` to get a statics pointer, or by getting a `ThreadStaticsInfo` to get a TLSIndex, which then can be used with the thread static variable system to get the actual thread static base.

```mermaid
classDiagram
MethodTable : MethodTableAuxiliaryData* m_pAuxData
MethodTable --> MethodTableAuxiliaryData
MethodTableAuxiliaryData --> DynamicStaticsInfo : If has static variables
MethodTableAuxiliaryData --> GenericStaticsInfo : If is generic and has static variables
MethodTableAuxiliaryData --> ThreadStaticsInfo : If has thread local static variables
DynamicStaticsInfo : StaticsPointer m_pGCStatics
DynamicStaticsInfo : StaticsPointer m_pNonGCStatics
GenericStaticsInfo : FieldDesc* m_pFieldDescs
ThreadStaticsInfo : TLSIndex NonGCTlsIndex
ThreadStaticsInfo : TLSIndex GCTlsIndex
```

```mermaid
classDiagram
note for StaticsPointer "StaticsPointer is a pointer sized integer"
StaticsPointer : void* PointerToStaticBase
StaticsPointer : bool HasClassConstructorBeenRun
note for TLSIndex "TLSIndex is a 32bit integer"
TLSIndex : TLSIndexType indexType
TLSIndex : 24bit int indexOffset
```

In the above diagram, you can see that we have separate fields for non-gc and gc statics, as well as thread and normal statics.
For normal statics, we use a single pointer sized field, which also happens to encode whether or not the class constructor has been run.
This is done to allow lock free atomic access to both get the static field address as well as determine if the class constructor needs to be triggered.
For TLS statics, handling of detecting whether or not the class constructor has been run is a more complex process described as part of the thread statics infrastructure.
The `DynamicStaticsInfo` and `ThreadStaticsInfo` structures are accessed without any locks, so it is important to ensure that access to fields on these structures can be done with a single memory access, to avoid memory order tearing issues.

Also, notably, for generic types, each field has a `FieldDesc` which is allocated per type instance, and is not shared by multiple canonical instances.

#### Lifetime management for collectible statics

Finally we have a concept of collectible assemblies in the CoreCLR runtime, so we need to handle lifetime management for static variables.
The approach chosen was to build a special GC handle type which will allow the runtime to have a pointer in the runtime data structures to the interior of a managed object on the GC heap.

The requirement of behavior here is that a static variable cannot keep its own collectible assembly alive, and so collectible statics have the peculiar property that they can exist and be finalized before the collectible assembly is finally collected.
If there is some resurrection scenario, this can lead to very surprising behavior.

### Thread Statics

Thread statics are static variables which have a lifetime which is defined to be the shorter of the lifetime of the type containing the static, and the lifetime of the thread on which the static variable is accessed.
They are created by having a static variable on a type which is attributed with `[System.Runtime.CompilerServices.ThreadStaticAttribute]`.
The general scheme of how this works is to assign an "index" to the type which is the same on all threads, and then on each thread hold a data structure which is efficiently accessed by means of this index.
However, we have a few peculiarities in our approach.

1. We segregate collectible and non-collectible thread statics (`TLSIndexType::NonCollectible` and `TLSIndexType::Collectible`)
2. We provide an ability to share a non-gc thread static between native CoreCLR code and managed code (Subset of `TLSIndexType::DirectOnThreadLocalData`)
3. We provide an extremely efficient means to access a small number of non-gc thread statics. (The rest of the usage of `TLSIndexType::DirectOnThreadLocalData`)

#### Per-Thread Statics Data structures
```mermaid
classDiagram
note for ThreadLocalInfo "There is 1 of these per thread, and it is managed by the C++ compiler/OS using standard mechanisms.
It can be found as the t_ThreadStatics variable in a C++ compiler, and is also pointed at by the native Thread class."
ThreadLocalInfo : int cNonCollectibleTlsData
ThreadLocalInfo : void** pNonCollectibleTlsArrayData
ThreadLocalInfo : int cCollectibleTlsData
ThreadLocalInfo : void** pCollectibleTlsArrayData
ThreadLocalInfo : InFlightTLSData *pInFightData
ThreadLocalInfo : Thread* pThread
ThreadLocalInfo : Special Thread Statics Shared Between Native and Managed code
ThreadLocalInfo : byte[N] ExtendedDirectThreadLocalTLSData
InFlightTLSData : InFlightTLSData* pNext
InFlightTLSData : TLSIndex tlsIndex
InFlightTLSData : OBJECTHANDLE hTLSData
ThreadLocalInfo --> InFlightTLSData : For TLS statics which have their memory allocated, but have not been accessed since the class finished running its class constructor
InFlightTLSData --> InFlightTLSData : linked list
```

#### Access patterns for getting the thread statics address

This is the pattern that the JIT will use to access a thread static which is not `DirectOnThreadLocalData`.

0. Get the TLS index somehow
1. Get TLS pointer to OS managed TLS block for the current thread ie. `pThreadLocalData = &t_ThreadStatics`
2. Read 1 integer value `pThreadLocalData->cCollectibleTlsData OR pThreadLocalData->cNonCollectibleTlsData`
3. Compare cTlsData against the index we're looking up `if (cTlsData < index.GetIndexOffset())`
4. If the index is not within range, jump to step 11.
5. Read 1 pointer value from TLS block `pThreadLocalData->pCollectibleTlsArrayData` OR `pThreadLocalData->pNonCollectibleTlsArrayData`
6. Read 1 pointer from within the TLS Array. `pTLSBaseAddress = *(intptr_t*)(((uint8_t*)pTlsArrayData) + index.GetIndexOffset()`
7. If pointer is NULL jump to step 11 `if pTLSBaseAddress == NULL`
8. If TLS index not a Collectible index, return pTLSBaseAddress
9. if `ObjectFromHandle((OBJECTHANDLE)pTLSBaseAddress)` is NULL, jump to step 11
10. Return `ObjectFromHandle((OBJECTHANDLE)pTLSBaseAddress)`
11. Tail-call a helper `return GetThreadLocalStaticBase(index)`

This is the pattern that the JIT will use to access a thread static which is on `DirectOnThreadLocalData`
0. Get the TLS index somehow
1. Get TLS pointer to OS managed TLS block for the current thread ie. `pThreadLocalData = &t_ThreadStatics`
2. Add the index offset to the start of the ThreadLocalData structure `pTLSBaseAddress = ((uint8_t*)pThreadLocalData) + index.GetIndexOffset()`

#### Lifetime management for thread static variables
We distinguish between collectible and non-collectible thread static variables for efficiency purposes.

A non-collectible thread static is a thread static defined on a type which cannot be collected by the runtime.
This describes most thread statics in actual observed practice.
The `DirectOnThreadLocalData` statics are a subset of this category which has a speical optimized form and does not need any GC reporting.
For non-collectible thread statics, the pointer (`pNonCollectibleTlsArrayData`) in the `ThreadLocalData` is a pointer to a managed `object[]` which points at either `object[]`, `byte[]`, or `double[]` arrays.
At GC scan time, the pointer to the initial object[] is the only detail which needs to be reported to the GC.

A collectible thread static is a thread static which can be collected by the runtime.
This describes the static variables defined on types which can be collected by the runtime.
The pointer (`pCollectibleTlsArrayData`) in the `ThreadLocalData` is a pointer to a chunk of memory allocated via `malloc`, and holds pointers to `object[]`, `byte[]`, or `double[]` arrays.
At GC scan time, each managed object must individually be kept alive only if the type and thread is still alive. This requires properly handling several situations.
1. If a collectible assembly becomes unreferenced, but a thread static variable associated with it has a finalizer, the object must move to the finalization queue.
2. If a thread static variable associated with a collectible assembly refers to the collectible assembly `LoaderAllocator` via a series of object references, it must not provide a reason for the collectible assembly to be considered referenced.
3. If a collectible assembly is collected, then the associated static variables no longer exist, and the TLSIndex values associated with that collectible assembly becomes re-useable.
4. If a thread is no longer executing, then all thread statics associated with that thread are no longer kept alive.

The approach chosen is to use a pair of different handle types.
For efficient access, the handle type stored in the dynamically adjusted array is a WeakTrackResurrection GCHandle.
This handle instance is associated with the slot in the TLS data, not with the exact instantiation, so it can be re-used when the if the associated collectible assembly is collected, and then the slot is re-used.
In addition, each slot that is in use will have a `LOADERHANDLE` which will keep the object alive until the `LoaderAllocator` is freed.
This `LOADERHANDLE` will be abandoned if the `LoaderAllocator` is collected, but that's ok, as `LOADERHANDLE` only needs to be cleaned up if the `LoaderAllocator` isn't collected.
On thread destroy, for each collectible slot in the tls array, we will explicitly free the `LOADERHANDLE` on the correct `LoaderAllocator`.

Physical Architecture
=====================
Expand All @@ -221,6 +345,7 @@ Major parts of the type system are found in:
- Array – Code for handling the special cases required for array processing
- VirtualStubDispatch.cpp/h/inl – Code for virtual stub dispatch
- VirtualCallStubCpu.hpp – Processor specific code for virtual stub dispatch.
- threadstatics.cpp/h - Handling for thread static variables.

Major entry points are BuildMethodTable, LoadTypeHandleThrowing, CanCastTo\*, GetMethodDescFromMemberDefOrRefOrSpecThrowing, GetFieldDescFromMemberRefThrowing, CompareSigs, and VirtualCallStubManager::ResolveWorkerStatic.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,8 +693,8 @@ internal unsafe struct MethodTableAuxiliaryData
[FieldOffset(0)]
private uint Flags;

private const uint enum_flag_CanCompareBitsOrUseFastGetHashCode = 0x0001; // Is any field type or sub field type overrode Equals or GetHashCode
private const uint enum_flag_HasCheckedCanCompareBitsOrUseFastGetHashCode = 0x0002; // Whether we have checked the overridden Equals or GetHashCode
private const uint enum_flag_CanCompareBitsOrUseFastGetHashCode = 0x0004; // Is any field type or sub field type overridden Equals or GetHashCode

public bool HasCheckedCanCompareBitsOrUseFastGetHashCode => (Flags & enum_flag_HasCheckedCanCompareBitsOrUseFastGetHashCode) != 0;

Expand Down
72 changes: 10 additions & 62 deletions src/coreclr/debug/daccess/request.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1715,8 +1715,8 @@ ClrDataAccess::GetModuleData(CLRDATA_ADDRESS addr, struct DacpModuleData *Module
ModuleData->bIsReflection = pModule->IsReflection();
ModuleData->bIsPEFile = pModule->IsPEFile();
ModuleData->Assembly = HOST_CDADDR(pModule->GetAssembly());
ModuleData->dwModuleID = pModule->GetModuleID();
ModuleData->dwModuleIndex = pModule->GetModuleIndex().m_dwIndex;
ModuleData->dwModuleID = 0; // CoreCLR no longer has this concept
ModuleData->dwModuleIndex = 0; // CoreCLR no longer has this concept
ModuleData->dwTransientFlags = pModule->m_dwTransientFlags;
ModuleData->LoaderAllocator = HOST_CDADDR(pModule->m_loaderAllocator);
ModuleData->ThunkHeap = HOST_CDADDR(pModule->m_pThunkHeap);
Expand Down Expand Up @@ -3246,47 +3246,16 @@ ClrDataAccess::GetNestedExceptionData(CLRDATA_ADDRESS exception, CLRDATA_ADDRESS
HRESULT
ClrDataAccess::GetDomainLocalModuleData(CLRDATA_ADDRESS addr, struct DacpDomainLocalModuleData *pLocalModuleData)
{
if (addr == 0 || pLocalModuleData == NULL)
return E_INVALIDARG;

SOSDacEnter();

DomainLocalModule* pLocalModule = PTR_DomainLocalModule(TO_TADDR(addr));

pLocalModuleData->pGCStaticDataStart = TO_CDADDR(PTR_TO_TADDR(pLocalModule->GetPrecomputedGCStaticsBasePointer()));
pLocalModuleData->pNonGCStaticDataStart = TO_CDADDR(pLocalModule->GetPrecomputedNonGCStaticsBasePointer());
pLocalModuleData->pDynamicClassTable = PTR_CDADDR(pLocalModule->m_pDynamicClassTable.Load());
pLocalModuleData->pClassData = (TADDR) (PTR_HOST_MEMBER_TADDR(DomainLocalModule, pLocalModule, m_pDataBlob));

SOSDacLeave();
return hr;
// CoreCLR does not use domain local modules anymore
return E_NOTIMPL;
}


HRESULT
ClrDataAccess::GetDomainLocalModuleDataFromModule(CLRDATA_ADDRESS addr, struct DacpDomainLocalModuleData *pLocalModuleData)
{
if (addr == 0 || pLocalModuleData == NULL)
return E_INVALIDARG;

SOSDacEnter();

Module* pModule = PTR_Module(TO_TADDR(addr));
DomainLocalModule* pLocalModule = PTR_DomainLocalModule(pModule->GetDomainLocalModule());
if (!pLocalModule)
{
hr = E_INVALIDARG;
}
else
{
pLocalModuleData->pGCStaticDataStart = TO_CDADDR(PTR_TO_TADDR(pLocalModule->GetPrecomputedGCStaticsBasePointer()));
pLocalModuleData->pNonGCStaticDataStart = TO_CDADDR(pLocalModule->GetPrecomputedNonGCStaticsBasePointer());
pLocalModuleData->pDynamicClassTable = PTR_CDADDR(pLocalModule->m_pDynamicClassTable.Load());
pLocalModuleData->pClassData = (TADDR) (PTR_HOST_MEMBER_TADDR(DomainLocalModule, pLocalModule, m_pDataBlob));
}

SOSDacLeave();
return hr;
// CoreCLR does not use domain local modules anymore
return E_NOTIMPL;
}

HRESULT
Expand All @@ -3299,31 +3268,8 @@ ClrDataAccess::GetDomainLocalModuleDataFromAppDomain(CLRDATA_ADDRESS appDomainAd
HRESULT
ClrDataAccess::GetThreadLocalModuleData(CLRDATA_ADDRESS thread, unsigned int index, struct DacpThreadLocalModuleData *pLocalModuleData)
{
if (pLocalModuleData == NULL)
return E_INVALIDARG;

SOSDacEnter();

pLocalModuleData->threadAddr = thread;
pLocalModuleData->ModuleIndex = index;

PTR_Thread pThread = PTR_Thread(TO_TADDR(thread));
PTR_ThreadLocalBlock pLocalBlock = ThreadStatics::GetCurrentTLB(pThread);
PTR_ThreadLocalModule pLocalModule = pLocalBlock->GetTLMIfExists(ModuleIndex(index));
if (!pLocalModule)
{
hr = E_INVALIDARG;
}
else
{
pLocalModuleData->pGCStaticDataStart = TO_CDADDR(PTR_TO_TADDR(pLocalModule->GetPrecomputedGCStaticsBasePointer()));
pLocalModuleData->pNonGCStaticDataStart = TO_CDADDR(pLocalModule->GetPrecomputedNonGCStaticsBasePointer());
pLocalModuleData->pDynamicClassTable = PTR_CDADDR(pLocalModule->m_pDynamicClassTable);
pLocalModuleData->pClassData = (TADDR) (PTR_HOST_MEMBER_TADDR(ThreadLocalModule, pLocalModule, m_pDataBlob));
}

SOSDacLeave();
return hr;
// CoreCLR does not use thread local modules anymore
return E_NOTIMPL;
}


Expand Down Expand Up @@ -3656,6 +3602,7 @@ static const char *LoaderAllocatorLoaderHeapNames[] =
{
"LowFrequencyHeap",
"HighFrequencyHeap",
"StaticsHeap",
"StubHeap",
"ExecutableHeap",
"FixupPrecodeHeap",
Expand Down Expand Up @@ -3690,6 +3637,7 @@ HRESULT ClrDataAccess::GetLoaderAllocatorHeaps(CLRDATA_ADDRESS loaderAllocatorAd
int i = 0;
pLoaderHeaps[i++] = HOST_CDADDR(pLoaderAllocator->GetLowFrequencyHeap());
pLoaderHeaps[i++] = HOST_CDADDR(pLoaderAllocator->GetHighFrequencyHeap());
pLoaderHeaps[i++] = HOST_CDADDR(pLoaderAllocator->GetStaticsHeap());
pLoaderHeaps[i++] = HOST_CDADDR(pLoaderAllocator->GetStubHeap());
pLoaderHeaps[i++] = HOST_CDADDR(pLoaderAllocator->GetExecutableHeap());
pLoaderHeaps[i++] = HOST_CDADDR(pLoaderAllocator->GetFixupPrecodeHeap());
Expand Down
Loading

0 comments on commit eb8f54d

Please sign in to comment.