Skip to content

LightEpoch: remove instance limit [dev]#1729

Open
badrishc wants to merge 2 commits intodevfrom
badrishc/unlimited-epoch
Open

LightEpoch: remove instance limit [dev]#1729
badrishc wants to merge 2 commits intodevfrom
badrishc/unlimited-epoch

Conversation

@badrishc
Copy link
Copy Markdown
Collaborator

Replace the fixed-size InstanceIndexBuffer [ThreadStatic] struct (previously limited to 16 concurrent instances) with a dynamically-growing GC-pinned int[] accessed via a [ThreadStatic] int* pointer. This removes the hard cap on concurrent LightEpoch instances entirely.

Design:

  • Per-thread entries array allocated as pinned int[] on first Resume() per thread, starting at capacity 16 and doubling as needed
  • Hot-path access is uniform pointer arithmetic: *(entriesPtr + instanceId), no branches
  • Instance IDs allocated via atomic counter with ConcurrentQueue recycling of disposed IDs to keep per-thread arrays compact
  • Double-dispose guarded via Interlocked.Exchange
  • ThisInstanceProtected() includes null + bounds checks for pre-Resume() calls

@badrishc badrishc force-pushed the badrishc/unlimited-epoch branch 5 times, most recently from 485a6c2 to 738ebf1 Compare April 29, 2026 01:43
Replace the fixed-size InstanceIndexBuffer [ThreadStatic] struct (previously
limited to 16 concurrent instances) with a dynamically-growing GC-pinned
int[] accessed via a [ThreadStatic] int* pointer. This removes the hard cap
on concurrent LightEpoch instances entirely.

Design:
- Per-thread entries array allocated as pinned int[] on first Resume() per
  thread, starting at capacity 16 and doubling as needed
- Hot-path access is uniform pointer arithmetic: *(entriesPtr + instanceId),
  no branches
- Instance IDs allocated via atomic counter with ConcurrentQueue recycling
  of disposed IDs to keep per-thread arrays compact
- Double-dispose guarded via Interlocked.Exchange
- ThisInstanceProtected() includes null + bounds checks for pre-Resume() calls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@badrishc badrishc force-pushed the badrishc/unlimited-epoch branch from 738ebf1 to 9b772b3 Compare April 29, 2026 19:21
@badrishc badrishc marked this pull request as ready for review May 3, 2026 04:17
Copilot AI review requested due to automatic review settings May 3, 2026 04:17
Copy link
Copy Markdown
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 removes the hard cap on concurrent LightEpoch instances by replacing the fixed-size per-thread instance-index buffer with a lazily allocated, dynamically growing GC-pinned int[] accessed via a [ThreadStatic] int* pointer. It updates both the Tsavorite core and client copies of LightEpoch to keep behavior aligned.

Changes:

  • Replaces the fixed-size [ThreadStatic] instance index buffer with a per-thread pinned int[] + int* pointer that grows by doubling.
  • Switches instance ID allocation to an atomic counter with a ConcurrentQueue<int> recycle pool and tracks active instances via an atomic counter.
  • Adds double-dispose guarding and updates ThisInstanceProtected() to include null + bounds checks.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
libs/storage/Tsavorite/cs/src/core/Epochs/LightEpoch.cs Implements dynamic per-thread pinned instance-entry map and new global instance ID allocation/recycling in Tsavorite core.
libs/client/LightEpoch.cs Mirrors the same dynamic per-thread pinned instance-entry map and instance ID allocation/recycling in the client copy.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread libs/client/LightEpoch.cs
void Release()
{
ref var entry = ref Metadata.Entries.GetRef(instanceId);
Debug.Assert(Metadata.entriesPtr != null, "Release called before Resume on this thread");
Comment thread libs/client/LightEpoch.cs
Comment on lines +669 to +672
var requiredCapacity = instanceId + 1;
var newCapacity = Math.Max(kInitialEntriesCapacity, Metadata.entriesCapacity);
while (newCapacity < requiredCapacity)
newCapacity *= 2;
Comment thread libs/client/LightEpoch.cs
Comment on lines +187 to 192
Interlocked.Increment(ref activeInstanceCount);
if (freeInstanceIds.TryDequeue(out var recycledId))
return recycledId;
return Interlocked.Increment(ref nextInstanceId) - 1;
}

Comment on lines +278 to 280
Debug.Assert(Metadata.entriesPtr != null, "ProtectAndDrain called before Resume on this thread");
ref var entry = ref *(Metadata.entriesPtr + instanceId);

void Release()
{
ref var entry = ref Metadata.Entries.GetRef(instanceId);
Debug.Assert(Metadata.entriesPtr != null, "Release called before Resume on this thread");
Comment on lines +670 to +673
var requiredCapacity = instanceId + 1;
var newCapacity = Math.Max(kInitialEntriesCapacity, Metadata.entriesCapacity);
while (newCapacity < requiredCapacity)
newCapacity *= 2;
Comment on lines 185 to +190
int SelectInstance()
{
for (var i = 0; i < InstanceIndexBuffer.MaxInstances; i++)
{
ref var entry = ref InstanceTracker.GetRef(i);
// Try to claim this instance ID (indicated as 1 in the entry)
if (kInvalidIndex == Interlocked.CompareExchange(ref entry, 1, kInvalidIndex))
return i;
}
throw new InvalidOperationException($"Exceeded maximum number of active LightEpoch instances {ActiveInstanceCount()} {InstanceIndexBuffer.MaxInstances}");
Interlocked.Increment(ref activeInstanceCount);
if (freeInstanceIds.TryDequeue(out var recycledId))
return recycledId;
return Interlocked.Increment(ref nextInstanceId) - 1;
Comment thread libs/client/LightEpoch.cs
Comment on lines +278 to 281
Debug.Assert(Metadata.entriesPtr != null, "ProtectAndDrain called before Resume on this thread");
ref var entry = ref *(Metadata.entriesPtr + instanceId);

Debug.Assert(entry > 0, "Trying to refresh unacquired epoch");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants