Replace CoreCLR SList with NativeAOT-style design#126949
Replace CoreCLR SList with NativeAOT-style design#126949max-charlamb wants to merge 6 commits intomainfrom
Conversation
Replace the sentinel-based CoreCLR SList (SLink/SList/SListElem) with the NativeAOT-style design: no sentinel node, NULL means empty, traits-based next-pointer access via DefaultSListTraits. The new SList uses offsetof(T, m_pNext) through DefaultSListTraits to locate the next pointer, and adds optional tail tracking via DefaultSListTraitsWithTail. All method names are preserved (InsertHead, InsertTail, RemoveHead, FindAndRemove, GetHead, GetNext, IsEmpty). Migrate all CoreCLR consumers: - CacheLine: SLink m_Link -> DPTR(CacheLine) m_pNext - CallCountingManager: -> CallCountingManagerList typedef - ConnectionCookie: remove CONTAINING_RECORD/InsertAfter patterns - StoredProfilerNode: head-only, simple migration - Thread: PTR_Thread m_pNext, update cdac_data - SyncBlock: raw SLink -> PTR_SyncBlock m_pNext, update DAC request.cpp - HandleCleanupListItem/FailedTypeInitCleanupListItem: -> WithTail - SListElem<NativeCodeVersion>: uses new SListElem wrapper Update cDAC contracts (Thread_1, SyncBlock_1) to remove SLink offset subtraction since links now point directly to objects. Remove SLink type from datadescriptor.inc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @agocke |
There was a problem hiding this comment.
Pull request overview
Replaces CoreCLR’s sentinel-based SList with a NativeAOT-style, traits-based design (no sentinel; NULL = empty) and migrates affected consumers and DAC/cDAC descriptors to point directly at the owning objects.
Changes:
- Reworked
src/coreclr/inc/slist.hto a traits-based intrusive list with optional tail tracking andSListElem<T>for non-intrusive usage. - Migrated multiple CoreCLR consumers from
SLink m_Linktom_pNext(DAC-safe pointer types) and updated list operations accordingly. - Updated DAC/cDAC contracts, descriptors, and mocks so list heads/links now point directly to objects (no offset subtraction /
CONTAINING_RECORD).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs | Updates mock thread store/list wiring to use direct Thread.Address links. |
| src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.SyncBlock.cs | Updates mock cleanup list to use direct SyncBlock.Address links. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs | Removes SLink offset logic; treats thread links as direct thread pointers. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/SyncBlock_1.cs | Removes link-offsetof subtraction; iterates cleanup list via direct pointers. |
| src/coreclr/vm/tieredcompilation.h | Introduces OptimizationQueue typedef using tail-enabled SList for optimization queue. |
| src/coreclr/vm/threads.h | Replaces SLink m_Link with PTR_Thread m_pNext; updates ThreadList and cdac offsets. |
| src/coreclr/vm/threads.cpp | Updates FindAndRemove usage to new boolean-return API. |
| src/coreclr/vm/syncblk.h | Replaces SLink usage with PTR_SyncBlock m_pNext; updates cdac offsets and cache list types. |
| src/coreclr/vm/syncblk.cpp | Removes offsetof(SyncBlock, m_Link) arithmetic; cleanup/free lists now store SyncBlock* directly. |
| src/coreclr/vm/loaderallocator.hpp | Replaces SLink with m_pNext in cleanup list items; adds tail-enabled list typedefs. |
| src/coreclr/vm/loaderallocator.cpp | Updates list iteration/removal to match new next-pointer layout. |
| src/coreclr/vm/datadescriptor/datadescriptor.inc | Removes SLink type and switches FirstThreadLink descriptor to a pointer. |
| src/coreclr/vm/comconnectionpoints.h | Replaces SLink with m_pNext in ConnectionCookie; updates list typedef. |
| src/coreclr/vm/comconnectionpoints.cpp | Removes CONTAINING_RECORD/InsertAfter usage; performs direct pointer chaining. |
| src/coreclr/vm/callcounting.h | Replaces SLink with m_pNext; introduces tail-enabled CallCountingManagerList. |
| src/coreclr/vm/callcounting.cpp | Updates static list declaration and iteration to new list type/API. |
| src/coreclr/vm/cachelinealloc.h | Replaces SLink with m_pNext; updates list typedefs and initialization. |
| src/coreclr/vm/cachelinealloc.cpp | Removes now-unneeded SList::Init() calls in allocator ctor. |
| src/coreclr/inc/slist.h | Core redesign: traits-based next-pointer access, optional tail tracking, SListElem<T>, and updated iterator/API. |
| src/coreclr/inc/profilepriv.h | Migrates StoredProfilerNode to m_pNext; updates profiler list typedef. |
| src/coreclr/debug/daccess/request.cpp | Updates DAC traversal of syncblock lists to use direct m_pNext pointers. |
| docs/design/datacontracts/SyncBlock.md | Updates docs to reflect cleanup list head now points to SyncBlock objects directly. |
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/9f34186d-66a2-4890-9a75-d27eff059bc5 Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
- Fix misleading friend-declaration comment in DefaultSListTraits - Fix Iterator operator++ DAC compat with dac_cast<PTR_T> - Fix static_assert message to say InsertTail instead of PushTail - Use DPTR for SListElem::m_pNext via local typedef - Simplify CallCountingManagerList:: references - Add SList::InsertAfter and use SList methods in comconnectionpoints.cpp instead of direct m_pNext manipulation - Remove HISTORY comment (per jkotas review) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // methods for fast list operations, with proper type checking | ||
| // | ||
| // see below for futher info. on how to use these template classes | ||
| // Unified singly linked list template. |
There was a problem hiding this comment.
Would it be possible to delete slist.* from under nativeaot and switch naot to this unified one?
There was a problem hiding this comment.
Yes, I wanted to make sure it doesn't break the coreclr use cases then I was going to have nativeaot redirect to this.
| private static TargetPointer GetThreadFromLink(TargetPointer threadLink) | ||
| { | ||
| if (threadLink == TargetPointer.Null) | ||
| return TargetPointer.Null; | ||
|
|
||
| // Get the address of the thread containing the link | ||
| return new TargetPointer(threadLink - _threadLinkOffset); | ||
| return threadLink; | ||
| } |
There was a problem hiding this comment.
We can remove this helper and access the element directly.
| // methods for fast list operations, with proper type checking | ||
| // | ||
| // see below for futher info. on how to use these template classes | ||
| // Unified singly linked list template. |
There was a problem hiding this comment.
Yes, I wanted to make sure it doesn't break the coreclr use cases then I was going to have nativeaot redirect to this.
| WRAPPER_NO_CONTRACT; | ||
| static_assert(!Traits::HasTail, "InsertHeadInterlocked is incompatible with tail tracking"); | ||
| _SLIST_ASSERT(pItem != NULL); | ||
| _SLIST_IS_ALIGNED(&m_pHead, sizeof(void*)); |
There was a problem hiding this comment.
InsertHeadInterlocked currently calls _SLIST_IS_ALIGNED(&m_pHead, sizeof(void*)) but does not assert on the result. This loses the safety check present in the previous implementation (and turns it into a no-op). Consider changing this to _SLIST_ASSERT(_SLIST_IS_ALIGNED(&m_pHead, sizeof(void*))) so misalignment is caught in debug builds.
| _SLIST_IS_ALIGNED(&m_pHead, sizeof(void*)); | |
| _SLIST_ASSERT(_SLIST_IS_ALIGNED(&m_pHead, sizeof(void*))); |
| // We can't afford to use an SList<> here because we only want to burn | ||
| // space for the minimum, which is the pointer within an SLink. |
There was a problem hiding this comment.
This comment still refers to the removed SLink type ("pointer within an SLink"), but the implementation now uses SyncBlock::m_pNext directly. Update the wording to avoid referencing SLink so the comment matches the new design.
| // We can't afford to use an SList<> here because we only want to burn | |
| // space for the minimum, which is the pointer within an SLink. | |
| // We can't afford extra list-node overhead here, so we use the minimum | |
| // space required: a single next pointer in the SyncBlock itself. |
This comment has been minimized.
This comment has been minimized.
cb65de6 to
84d3e34
Compare
4e843f7 to
b73b3b5
Compare
| // Next pointer for SList linkage. | ||
| DPTR(ConnectionCookie) m_pNext; | ||
| OBJECTHANDLEHolder m_hndEventProvObj; |
There was a problem hiding this comment.
ConnectionCookie::m_pNext is never initialized. Because ConnectionPoint::InsertWithLock can call CONNECTIONCOOKIELIST::InsertAfter, the list code asserts that the new item’s next pointer is NULL; with the current user-defined constructor, m_pNext will contain indeterminate data and can trip the assertion (and potentially corrupt the list in retail). Initialize m_pNext to NULL/nullptr in the ConnectionCookie constructor initializer list (or set it explicitly before inserting).
This comment has been minimized.
This comment has been minimized.
79af0ef to
8e46933
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/coreclr/vm/syncblk.h:459
SyncBlock::m_pNextreplaces the oldSLinkmember but is never initialized inSyncBlock(DWORD indx). Newly created SyncBlocks can therefore have a garbagem_pNextvalue, which can break diagnostics/list traversal (e.g., DAC code inrequest.cppchecks/traversesm_pNext, and cDACLinkNextnow maps directly to this field). Initializem_pNexttoNULLin the constructor initializer list (and/or otherwise ensure it is cleared for in-use SyncBlocks).
public:
// When the SyncBlock is released (we recycle them),
// the SyncBlockCache maintains free and cleanup lists of SyncBlocks
// using the m_pNext pointer below.
// Next pointer for SList linkage.
PTR_SyncBlock m_pNext;
protected:
// This is the hash code for the object. It can either have been transferred
// from the header dword, in which case it will be limited to 26 bits, or
// have been generated right into this member variable here, when it will
// be a full 32 bits.
// A 0 in this variable means no hash code has been set yet - this saves having
// another flag to express this state, and it enables us to use a 32-bit interlocked
// operation to set the hash code, on the other hand it means that hash codes
// can never be 0. ObjectNative::GetHashCode in objectnative.cpp makes sure to enforce this.
DWORD m_dwHashCode;
public:
SyncBlock(DWORD indx)
: m_Lock((OBJECTHANDLE)NULL)
, m_thinLock()
, m_dwSyncIndex(indx)
#ifdef FEATURE_METADATA_UPDATER
, m_pEnCInfo(PTR_NULL)
#endif // FEATURE_METADATA_UPDATER
, m_dwHashCode(0)
{
LIMITED_METHOD_CONTRACT;
m_pInteropInfo = NULL;
}
| // Next pointer for SList linkage. | ||
| DPTR(ConnectionCookie) m_pNext; | ||
| OBJECTHANDLEHolder m_hndEventProvObj; | ||
| DWORD m_id; |
There was a problem hiding this comment.
ConnectionCookie::m_pNext is newly introduced but never initialized by the ConnectionCookie(OBJECTHANDLEHolder) constructor. This can trip the _ASSERTE(*Traits::GetNextPtr(pNewItem) == NULL) in SList::InsertAfter (used in ConnectionPoint::InsertWithLock) and leaves an uninitialized pointer field on newly created cookies. Initialize m_pNext to NULL in the constructor initializer list (and/or ensure it is cleared before insertion).
| { return it.Insert(pItem); } | ||
|
|
||
| // Removes item pointed to by it. Returns iterator to following item. | ||
| Iterator Remove(Iterator & it) | ||
| { return it.Remove(); } |
There was a problem hiding this comment.
For Traits::HasTail == true, the iterator-based mutation APIs (SList::Insert(Iterator&, ...) / SList::Remove(Iterator&)) do not maintain m_pTail. Removing or inserting via these APIs can leave m_pTail stale, breaking subsequent InsertTail() / GetTail() semantics. Either update these APIs to preserve tail correctness (e.g., have Iterator carry a back-pointer to the owning SList and update tail when removing/inserting at the end), or disallow them for tail-tracking lists via static_assert(!Traits::HasTail, ...) and document the restriction.
| { return it.Insert(pItem); } | |
| // Removes item pointed to by it. Returns iterator to following item. | |
| Iterator Remove(Iterator & it) | |
| { return it.Remove(); } | |
| { | |
| static_assert(!Traits::HasTail, | |
| "SList::Insert(Iterator&, ...) is not supported for tail-tracking lists because it cannot maintain m_pTail."); | |
| return it.Insert(pItem); | |
| } | |
| // Removes item pointed to by it. Returns iterator to following item. | |
| Iterator Remove(Iterator & it) | |
| { | |
| static_assert(!Traits::HasTail, | |
| "SList::Remove(Iterator&) is not supported for tail-tracking lists because it cannot maintain m_pTail."); | |
| return it.Remove(); | |
| } |
8e46933 to
c288fd6
Compare
c288fd6 to
978d795
Compare
| SList<SyncBlock> m_CleanupBlockList; // list of sync blocks that need cleanup | ||
| SList<SyncBlock> m_FreeBlockList; // list of free sync blocks |
There was a problem hiding this comment.
SyncBlockCache is documented as having "No constructors/destructors - global instance" (see below in this type), but adding SList<SyncBlock> members makes SyncBlockCache non-trivially constructible due to SList's constructor. This can introduce a global constructor for g_SyncBlockCacheInstance and violates the stated constraint. Consider keeping these as raw pointers (as before) or introducing a trivially-initializable SList variant that can be explicitly initialized from SyncBlockCache::Init() without requiring static initialization.
| SList<SyncBlock> m_CleanupBlockList; // list of sync blocks that need cleanup | |
| SList<SyncBlock> m_FreeBlockList; // list of free sync blocks | |
| SyncBlock *m_CleanupBlockList; // head of sync blocks that need cleanup | |
| SyncBlock *m_FreeBlockList; // head of free sync blocks |
| // Inserts pNewItem immediately after pAfter in the list. | ||
| static void InsertAfter(PTR_T pAfter, PTR_T pNewItem) | ||
| { | ||
| static_assert(!Traits::IsInterlocked, "InsertAfter is not safe on interlocked lists"); | ||
| LIMITED_METHOD_CONTRACT; | ||
| _ASSERTE(pAfter != NULL && pNewItem != NULL); | ||
| _ASSERTE(*Traits::GetNextPtr(pNewItem) == NULL); | ||
| *Traits::GetNextPtr(pNewItem) = *Traits::GetNextPtr(pAfter); | ||
| *Traits::GetNextPtr(pAfter) = pNewItem; |
There was a problem hiding this comment.
SList::InsertAfter is available for tail-tracking lists (SListMode::Tail), but because it is static it cannot update the list's cached tail pointer. Calling this on a tail list (especially inserting after the current tail) would leave m_pTail stale and break subsequent InsertTail/GetTail behavior. Either disallow InsertAfter when Traits::HasTail (static_assert), or make it an instance method that updates the tail when needed.
This comment has been minimized.
This comment has been minimized.
978d795 to
58f4cf5
Compare
This comment has been minimized.
This comment has been minimized.
58f4cf5 to
fe372b5
Compare
| #define WRAPPER_NO_CONTRACT | ||
| #endif | ||
|
|
||
| #include "cdacdata.h" |
There was a problem hiding this comment.
slist.h includes "cdacdata.h", but there is no src/coreclr/inc/cdacdata.h; it currently lives at src/coreclr/vm/cdacdata.h. This will compile in CoreCLR builds only if the VM include directory is on the include path, and is likely to break NativeAOT (which includes this header via a relative path but does not add src/coreclr/vm to include directories). Prefer including the header via a stable relative path (e.g., ../vm/cdacdata.h) or forward-declaring template<typename T> struct cdac_data; to avoid the include-path dependency.
| #include "cdacdata.h" | |
| #include "../vm/cdacdata.h" |
fe372b5 to
9338405
Compare
This comment has been minimized.
This comment has been minimized.
9338405 to
b427bc7
Compare
| // SList has two different behaviours depending on boolean | ||
| // fHead variable, | ||
| // Intrusive singly linked list. Elements must have a pointer-sized m_pNext | ||
| // field. Use DefaultSListTraitsWithTail<T> for O(1) tail insertion. |
There was a problem hiding this comment.
Comment mentions DefaultSListTraitsWithTail<T> for tail insertion, but the new API surface provides SListTail<T> / SListTraits<T, SListMode::Tail> (and there is no DefaultSListTraitsWithTail in this header). Updating the comment would avoid misleading future callers.
| // field. Use DefaultSListTraitsWithTail<T> for O(1) tail insertion. | |
| // field. Use SListTraits<T, SListMode::Tail> for O(1) tail insertion. |
🤖 Copilot Code Review — PR #126949Note This review was generated by Copilot (Claude Opus 4.6), with additional findings synthesized from a Claude Sonnet 4.5 sub-agent review. Holistic AssessmentMotivation: Justified — unifying the SList data structure between CoreCLR and NativeAOT reduces duplication and the Approach: Sound — the traits-based design with Summary: Detailed Findings❌
|
- Simplify compat macros: drop #ifndef guards, drop _ASSERTE alias, move forward_declarations.h to RuntimeInstance.h - Make m_pHead volatile in non-DAC builds (restores NativeAOT semantics) - Move m_pHead to top of class (matches original layout) - Guard m_fIsValid references with #ifdef _DEBUG - Restore InsertAfter null-next assert - Change while(true) to for(;;) in slist.inl - Unify method names: drop PushHead/PopHead/PushHeadInterlocked aliases, update NativeAOT call sites to use InsertHead/RemoveHead/InsertHeadInterlocked - Drop Begin()/End(), keep only lowercase begin()/end() - Remove GetThreadFromLink helper, inline directly - Use HandleCleanupList::GetNext instead of item->m_pNext - Fix Thread_1.cs indentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b427bc7 to
62bbb1d
Compare
| @@ -290,8 +290,7 @@ void SyncBlockCache::Init() | |||
| } | |||
| CONTRACTL_END; | |||
|
|
|||
| m_pCleanupBlockList = NULL; | |||
| m_FreeBlockList = NULL; | |||
| // m_CleanupBlockList and m_FreeBlockList are default-constructed (empty). | |||
There was a problem hiding this comment.
don't need comment, can be removed.
| m_FreeBlockList = NULL; | ||
| //<TODO>@todo we can clear this fast too I guess</TODO> | ||
| m_pCleanupBlockList = NULL; | ||
| // m_CleanupBlockList and m_FreeBlockList are destroyed with the SyncBlockCache. |
There was a problem hiding this comment.
same as above, can be removed.
| SList<SyncBlock> m_CleanupBlockList; // list of sync blocks that need cleanup | ||
| SList<SyncBlock> m_FreeBlockList; // list of free sync blocks |
There was a problem hiding this comment.
nit: please fix comment spacing
Note
This PR was authored with the assistance of GitHub Copilot.
Summary
Unify the CoreCLR and NativeAOT
SListimplementations into a single shared header atsrc/coreclr/inc/slist.h. NativeAOT'sslist.hbecomes a redirect to this shared header. The old sentinel-basedSLink/SListdesign is replaced with the NativeAOT-style design: no sentinel node,NULLmeans empty, traits-based next-pointer access.Design
SListTraits and SListMode
A single
SListTraits<T, Mode, FailFastPolicy>template replaces the oldDefaultSListTraits/DefaultSListTraitsWithTailclasses. TheSListModeenum controls which operations are permitted:Thin(default)TailInterlockedInvalid combinations (e.g., calling InsertHead on an Interlocked list) are caught at compile time via
static_assert.Convenience Aliases
Other Features
begin()/end(),Insert/Removeat position, debug validationInsertHeadInterlockedvia CAS (PalInterlockedCompareExchangePointeron NativeAOT,InterlockedCompareExchangeTon CoreCLR), implemented inslist.inlSListElem<T>wrapper for non-intrusive list usagem_pHeadisvolatilein non-DAC builds for correct iteration concurrent with interlocked insertsSListTailBaseconditional storage —m_pTailonly exists whenHasTailis trueConsumer Migrations
All method names preserved:
InsertHead,InsertTail,RemoveHead,FindAndRemove,GetHead,GetNext,IsEmpty.m_pNexttypeDPTR(CacheLine)SList<CacheLine>DPTR(CallCountingManager)SListTail<CallCountingManager>DPTR(ConnectionCookie)SList<ConnectionCookie>DPTR(StoredProfilerNode)SList<StoredProfilerNode>PTR_ThreadSListTail<Thread>PTR_SyncBlockDPTR(...)SListTail<HandleCleanupListItem>DPTR(...)SListTail<FailedTypeInitCleanupListItem>DPTR(SListElem)SListTail<SListElem<NativeCodeVersion>>PTR_OsModuleEntry volatileSListInterlocked<OsModuleEntry>TypeManagerEntry* volatileSListInterlocked<TypeManagerEntry>DAC / cDAC Changes
SyncBlock_1.cs,Thread_1.cs: Removed SLink offset subtraction (links now point directly to objects)datadescriptor.inc: RemovedSLinktype definition,ThreadStore.FirstThreadLinkchanged fromTYPE(SLink)toT_POINTERLinkAddress/CleanupLinkAddresspropertiesNativeAOT Unification
nativeaot/Runtime/slist.hredirects to../../inc/slist.hnativeaot/Runtime/slist.inlreduced torh::std::find/countutilities + includes shared../../inc/slist.inlInsertHead/RemoveHead/InsertHeadInterlocked/begin/end)