Memregion pool no handle (#26110)

Co-authored-by: SirOlaf <34164198+SirOlaf@users.noreply.github.com>
This commit is contained in:
Andreas Rumpf
2026-08-17 12:22:53 +02:00
committed by GitHub
parent 16920b56d1
commit 43f7631b1c
8 changed files with 460 additions and 59 deletions

View File

@@ -1154,7 +1154,19 @@ template sysAssert(cond: bool, msg: string) =
cstderr.rawWrite "\n"
rawQuit 1
const hasAlloc = (hostOS != "standalone" or not defined(nogc)) and not defined(nimscript)
const
hasAlloc = (hostOS != "standalone" or not defined(nogc)) and not defined(nimscript)
hasDefaultAllocator =
hasAlloc and
not (defined(useNimRtl) or defined(useMalloc) or defined(gcRegions) or
defined(nogc) or defined(boehmgc) or defined(gogc))
hasThreadLocalAllocator =
hasDefaultAllocator and hasThreadSupport and defined(gcDestructors)
when hasThreadLocalAllocator:
# threadimpl is included before mmdisp provides these implementations.
proc initThreadAllocator() {.gcsafe, raises: [].}
proc releaseThreadAllocator() {.gcsafe, raises: [].}
when notJSnotNims and hasAlloc and not defined(nimSeqsV2):
proc addChar(s: NimString, c: char): NimString {.compilerproc, gcsafe.}
@@ -2425,6 +2437,8 @@ when notJSnotNims and hasAlloc:
{.push profiler: off.}
include "system/mmdisp"
{.pop.}
when hasThreadLocalAllocator:
initThreadAllocator()
{.push stackTrace: off, profiler: off.}
when not defined(nimSeqsV2):
include "system/sysstr"

View File

@@ -40,7 +40,7 @@ template track(op, address, size) =
#
# A deallocation of a small pointer then looks like this
#[
dealloc -> rawDealloc -> chunk.owner == addr(a) --------------> This thread owns the chunk ------> The current chunk is active -> Chunk is completely unused -----> Chunk references no foreign cells
dealloc -> rawDealloc -> chunk.owner == regionOwner(a) -------> This thread owns the chunk ------> The current chunk is active -> Chunk is completely unused -----> Chunk references no foreign cells
| | (Add cell into the current chunk) | Return the current chunk back to tlsf
| | | |
v v v v
@@ -63,6 +63,11 @@ const
# size of chunks in last matrix bin
MaxBigChunkSize = int(1'i32 shl MaxFli - 1'i32 shl (MaxFli-MaxLog2Sli-1))
HugeChunkSize = MaxBigChunkSize + 1
usesRegionHandles = hasThreadSupport and defined(gcDestructors)
# Deliberately *not* `hasThreadLocalAllocator`: this selects the chunk
# layout, which is ABI and must match between a `--useNimRtl` client and
# the RTL it links against. Whether this module owns a thread local region
# is the separate question that `hasThreadLocalAllocator` answers.
type
PTrunk = ptr Trunk
@@ -112,11 +117,15 @@ type
PChunk = ptr BaseChunk
PBigChunk = ptr BigChunk
PSmallChunk = ptr SmallChunk
SharedFreeLists = array[0..max(1, SmallChunkSize div MemAlign-1), ptr FreeCell]
BaseChunk {.pure, inheritable.} = object
prevSize: int # size of previous chunk; for coalescing
# 0th bit == 1 if 'used
size: int # if < PageSize it is a small chunk
owner: ptr MemRegion
when usesRegionHandles:
owner: ptr RegionHandle
else:
owner: ptr MemRegion
SmallChunk = object of BaseChunk
next, prev: PSmallChunk # chunks of the same size
@@ -145,14 +154,16 @@ type
next: ptr HeapLinks
MemRegion = object
when usesRegionHandles:
regionHandle: ptr RegionHandle
when not defined(gcDestructors):
minLargeObj, maxLargeObj: int
freeSmallChunks: array[0..max(1, SmallChunkSize div MemAlign-1), PSmallChunk]
# List of available chunks per size class. Only one is expected to be active per class.
when defined(gcDestructors):
sharedFreeLists: array[0..max(1, SmallChunkSize div MemAlign-1), ptr FreeCell]
# When a thread frees a pointer it did not create, it must not adjust the counters.
# Instead, the cell is placed here and deferred until the next allocation.
sharedFreeLists: SharedFreeLists
# Used directly without threads. Threaded builds use RegionHandle but
# retain this 2 KiB spacer: removing it regresses 2-4 KiB allocations.
flBitmap: uint32
slBitmap: array[RealFli, uint32]
matrix: array[RealFli, array[MaxSli, PBigChunk]]
@@ -160,7 +171,7 @@ type
currMem, maxMem, freeMem, occ: int # memory sizes (allocated from OS)
lastSize: int # needed for the case that OS gives us pages linearly
when defined(gcDestructors):
sharedFreeListBigChunks: PBigChunk # make no attempt at avoiding false sharing for now for this object field
sharedFreeListBigChunks: PBigChunk # private pending list with threads; shared queue otherwise
chunkStarts: IntSet
when not defined(gcDestructors):
@@ -173,9 +184,24 @@ type
when defined(nimTypeNames):
allocCounter, deallocCounter: int
RegionHandle = object
# Permanent chunk-owner identity and home of the remote-free queues.
sharedFreeLists: SharedFreeLists
sharedFreeListBigChunks: PBigChunk
# Keep the movable allocator state with its permanent owner while the
# owning thread is retired.
region: MemRegion
next: ptr RegionHandle
template smallChunkOverhead(): untyped = sizeof(SmallChunk)
template bigChunkOverhead(): untyped = sizeof(BigChunk)
template regionOwner(a: var MemRegion): untyped =
when usesRegionHandles:
a.regionHandle
else:
addr a
when hasThreadSupport:
template loada(x: untyped): untyped = atomicLoadN(unsafeAddr x, ATOMIC_RELAXED)
template storea(x, y: untyped) = atomicStoreN(unsafeAddr x, y, ATOMIC_RELAXED)
@@ -502,6 +528,56 @@ proc pageAddr(p: pointer): PChunk {.inline.} =
result = cast[PChunk](cast[int](p) and not PageMask)
#sysAssert(Contains(allocator.chunkStarts, pageIndex(result)))
when hasThreadLocalAllocator:
var
regionPool: ptr RegionHandle
regionPoolLock: SysLock
initSysLock(regionPoolLock)
proc moveMemRegion(dest, source: ptr MemRegion) {.inline.} =
# MemRegion owns only raw allocator state, so transfer it bitwise and
# clear the source to leave exactly one owner.
copyMem(dest, source, sizeof(MemRegion))
zeroMem(source, sizeof(MemRegion))
proc acquireMemRegion(a: var MemRegion) {.raises: [], gcsafe.} =
if a.regionHandle != nil:
return
acquireSys(regionPoolLock)
let handle = regionPool
if handle != nil:
regionPool = handle.next
releaseSys(regionPoolLock)
if handle == nil:
# RegionHandle is larger than llAlloc's one-page metadata slabs and is
# retained independently of any checked-out MemRegion.
let handleSize = roundup(sizeof(RegionHandle), PageSize)
let newHandle = cast[ptr RegionHandle](osAllocPages(handleSize))
zeroMem(newHandle, sizeof(RegionHandle))
a.regionHandle = newHandle
else:
moveMemRegion(addr a, addr handle.region)
proc releaseMemRegion(a: var MemRegion) {.raises: [], gcsafe.} =
# Zeroing `a` also clears `a.regionHandle`, which is what keeps a late
# `dealloc` on this thread correct: the ownership test can no longer match,
# so the cell is routed to its real owner's handle instead of to a region
# that is about to be reused. A late *alloc* on the other hand would mint
# chunks with a nil owner, so nothing may allocate after this point --
# `afterThreadRuns` has already run by the time `threadProcWrapStackFrame`
# gets here.
if a.regionHandle == nil:
return
let handle = a.regionHandle
moveMemRegion(addr handle.region, addr a)
acquireSys(regionPoolLock)
handle.next = regionPool
regionPool = handle
releaseSys(regionPoolLock)
when false:
proc writeFreeList(a: MemRegion) =
var it = a.freeChunksList
@@ -618,7 +694,7 @@ proc splitChunk2(a: var MemRegion, c: PBigChunk, size: int): PBigChunk =
result.prev = nil
# size and not used:
result.prevSize = size
result.owner = addr a
result.owner = regionOwner(a)
sysAssert((size and 1) == 0, "splitChunk 2")
sysAssert((size and PageMask) == 0,
"splitChunk: size is not a multiple of the PageSize")
@@ -686,7 +762,7 @@ proc getBigChunk(a: var MemRegion, size: int): PBigChunk =
# if we over allocated split the chunk:
if result.size > size:
splitChunk(a, result, size)
result.owner = addr a
result.owner = regionOwner(a)
else:
removeChunkFromMatrix2(a, result, fl, sl)
if result.size >= size + PageSize:
@@ -694,7 +770,7 @@ proc getBigChunk(a: var MemRegion, size: int): PBigChunk =
# set 'used' to true:
result.prevSize = 1
track("setUsedToFalse", addr result.size, sizeof(int))
sysAssert result.owner == addr a, "getBigChunk: No owner set!"
sysAssert result.owner == regionOwner(a), "getBigChunk: No owner set!"
incl(a, a.chunkStarts, pageIndex(result))
dec(a.freeMem, size)
@@ -710,7 +786,7 @@ proc getHugeChunk(a: var MemRegion; size: int): PBigChunk =
result.size = size
# set 'used' to true:
result.prevSize = 1
result.owner = addr a
result.owner = regionOwner(a)
incl(a, a.chunkStarts, pageIndex(result))
proc freeHugeChunk(a: var MemRegion; c: PBigChunk) =
@@ -791,7 +867,7 @@ proc deallocBigChunk(a: var MemRegion, c: PBigChunk) =
when defined(gcDestructors):
template atomicPrepend(head, elem: untyped) =
# see also https://en.cppreference.com/w/cpp/atomic/atomic_compare_exchange
when hasThreadSupport:
when usesRegionHandles:
while true:
elem.next.storea head.loada
if atomicCompareExchangeN(addr head, addr elem.next, elem, weak = true, ATOMIC_RELEASE, ATOMIC_RELAXED):
@@ -800,30 +876,39 @@ when defined(gcDestructors):
elem.next.storea head.loada
head.storea elem
proc addToSharedFreeListBigChunks(a: var MemRegion; c: PBigChunk) {.inline.} =
sysAssert c.next == nil, "c.next pointer must be nil"
atomicPrepend a.sharedFreeListBigChunks, c
when usesRegionHandles:
proc addToSharedFreeListBigChunks(handle: ptr RegionHandle;
c: PBigChunk) {.inline.} =
sysAssert c.next == nil, "c.next pointer must be nil"
atomicPrepend handle.sharedFreeListBigChunks, c
else:
proc addToSharedFreeListBigChunks(a: var MemRegion;
c: PBigChunk) {.inline.} =
sysAssert c.next == nil, "c.next pointer must be nil"
atomicPrepend a.sharedFreeListBigChunks, c
proc takeFromSharedFreeListBigChunks(a: var MemRegion): PBigChunk {.inline.} =
when hasThreadSupport:
while true:
result = atomicLoadN(addr a.sharedFreeListBigChunks, ATOMIC_ACQUIRE)
if result == nil:
break
let next = result.next.loada
var expected = result
if atomicCompareExchangeN(addr a.sharedFreeListBigChunks, addr expected, next,
weak = true, ATOMIC_ACQUIRE, ATOMIC_RELAXED):
result.next.storea nil
break
else:
result = a.sharedFreeListBigChunks
if result != nil:
a.sharedFreeListBigChunks = result.next
result.next = nil
when usesRegionHandles:
if a.sharedFreeListBigChunks == nil:
let sharedHead = addr a.regionHandle.sharedFreeListBigChunks
# Detach a batch from the stable remote inbox. The embedded MemRegion
# field is now a private pending list and moves with the region.
if atomicLoadN(sharedHead, ATOMIC_RELAXED) != nil:
a.sharedFreeListBigChunks = atomicExchangeN(sharedHead, nil,
ATOMIC_ACQUIRE)
result = a.sharedFreeListBigChunks
if result != nil:
a.sharedFreeListBigChunks = result.next
result.next = nil
proc addToSharedFreeList(c: PSmallChunk; f: ptr FreeCell; size: int) {.inline.} =
atomicPrepend c.owner.sharedFreeLists[size], f
when usesRegionHandles:
proc addToSharedFreeList(handle: ptr RegionHandle; f: ptr FreeCell;
size: int) {.inline.} =
atomicPrepend handle.sharedFreeLists[size], f
else:
proc addToSharedFreeList(c: PSmallChunk; f: ptr FreeCell;
size: int) {.inline.} =
atomicPrepend c.owner.sharedFreeLists[size], f
const MaxSteps = 20
@@ -846,9 +931,8 @@ when defined(gcDestructors):
dec(a.occ, total)
proc freeDeferredObjects(a: var MemRegion) =
# Pop only as many nodes as we can process. Detaching the entire list and
# re-enqueuing its unprocessed tail through atomicPrepend would overwrite
# that tail's next pointer and lose the rest of the list.
# Bound the work per allocation. With threads, takeFromSharedFreeListBigChunks
# detaches the shared stack into the region's private pending list first.
for _ in 0..MaxSteps:
let it = takeFromSharedFreeListBigChunks(a)
if it == nil: break
@@ -892,17 +976,20 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
if size + alignOff <= SmallChunkSize-smallChunkOverhead():
template fetchSharedCells(tc: PSmallChunk) =
# Consumes cells from (potentially) foreign threads from `a.sharedFreeLists[s]`
# Consume cells freed by potentially foreign threads.
when defined(gcDestructors):
if tc.freeList == nil:
when hasThreadSupport:
# Steal the entire list from `sharedFreeList`:
tc.freeList = atomicExchangeN(addr a.sharedFreeLists[s], nil, ATOMIC_RELAXED)
when usesRegionHandles:
let sharedHead = addr tc.owner.sharedFreeLists[s]
# The owner is the only consumer, so once it observes a non-empty
# stack no other thread can make it empty before the exchange.
if atomicLoadN(sharedHead, ATOMIC_RELAXED) != nil:
tc.freeList = atomicExchangeN(sharedHead, nil, ATOMIC_ACQUIRE)
else:
tc.freeList = a.sharedFreeLists[s]
a.sharedFreeLists[s] = nil
# if `tc.freeList` isn't nil, `tc` will gain capacity.
# We must calculate how much it gained and how many foreign cells are included.
# If `tc.freeList` isn't nil, `tc` gains capacity. Calculate how
# much it gained and how many foreign cells are included.
compensateCounters(a, tc, size)
# allocate a small block: for small chunks, we use only its next pointer
@@ -921,11 +1008,11 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
c.size = size
c.acc = (alignOff + size).uint32
c.free = SmallChunkSize - smallChunkOverhead() - alignOff.int32 - size.int32
sysAssert c.owner == addr(a), "rawAlloc: No owner set!"
sysAssert c.owner == regionOwner(a), "rawAlloc: No owner set!"
c.next = nil
c.prev = nil
# Shared cells are fetched here in case `c.size * 2 >= SmallChunkSize - smallChunkOverhead()`.
# For those single cell chunks, we would otherwise have to allocate a new one almost every time.
# Fetch deferred cells here for single-cell chunks; otherwise every
# allocation of that size would tend to allocate a new chunk.
fetchSharedCells(c)
if c.free >= size:
# Because removals from `a.freeSmallChunks[s]` only happen in the other alloc branch and during dealloc,
@@ -963,9 +1050,8 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer
dec(c.free, size)
sysAssert((cast[int](result) and (MemAlign-1)) == 0, "rawAlloc 9")
sysAssert(allocInv(a), "rawAlloc: end c != nil")
# We fetch deferred cells *after* advancing `c.freeList`/`acc` to adjust `c.free`.
# If after the adjustment it turns out there's free cells available,
# the chunk stays in `a.freeSmallChunks[s]` and the need for a new chunk is delayed.
# Fetch after advancing `freeList`/`acc` so `c.free` can be adjusted. If
# cells arrived, keep this chunk active instead of allocating another.
fetchSharedCells(c)
sysAssert(allocInv(a), "rawAlloc: before c.free < size")
if c.free < size:
@@ -1030,7 +1116,8 @@ proc rawDealloc(a: var MemRegion, p: pointer) =
# ^ We might access thread foreign storage here.
# The other thread cannot possibly free this block as it's still alive.
var f = cast[ptr FreeCell](p)
if c.owner == addr(a):
let owner = c.owner
if owner == regionOwner(a):
# We own the block, there is no foreign thread involved.
dec a.occ, s
untrackSize(s)
@@ -1093,7 +1180,10 @@ proc rawDealloc(a: var MemRegion, p: pointer) =
when logAlloc: cprintf("dealloc(pointer_%p) # SMALL FROM %p CALLER %p\n", p, c.owner, addr(a))
when defined(gcDestructors):
addToSharedFreeList(c, f, s div MemAlign)
when usesRegionHandles:
addToSharedFreeList(owner, f, s div MemAlign)
else:
addToSharedFreeList(c, f, s div MemAlign)
sysAssert(((cast[int](p) and PageMask) - smallChunkOverhead() - c.chunkAlignOff) %%
s == 0, "rawDealloc 2")
else:
@@ -1101,10 +1191,14 @@ proc rawDealloc(a: var MemRegion, p: pointer) =
when overwriteFree: nimSetMem(p, -1'i32, c.size -% bigChunkOverhead())
when logAlloc: cprintf("dealloc(pointer_%p) # BIG %p\n", p, c.owner)
when defined(gcDestructors):
if c.owner == addr(a):
let owner = c.owner
if owner == regionOwner(a):
deallocBigChunk(a, cast[PBigChunk](c))
else:
addToSharedFreeListBigChunks(c.owner[], cast[PBigChunk](c))
when usesRegionHandles:
addToSharedFreeListBigChunks(owner, cast[PBigChunk](c))
else:
addToSharedFreeListBigChunks(owner[], cast[PBigChunk](c))
else:
deallocBigChunk(a, cast[PBigChunk](c))
@@ -1263,6 +1357,13 @@ when defined(nimTypeNames):
template instantiateForRegion(allocator: untyped) {.dirty.} =
{.push stackTrace: off.}
when hasThreadLocalAllocator:
proc initThreadAllocator() {.gcsafe, raises: [].} =
acquireMemRegion(allocator)
proc releaseThreadAllocator() {.gcsafe, raises: [].} =
releaseMemRegion(allocator)
when defined(nimFulldebug):
proc interiorAllocatedPtr*(p: pointer): pointer =
result = interiorAllocatedPtr(allocator, p)

View File

@@ -312,13 +312,17 @@ when not (defined(gcOrc) or defined(gcYrc)):
## Forces a full garbage collection pass. With `--mm:arc` a nop.
discard
template setupForeignThreadGc* =
## With `--mm:arc` a nop.
discard
template tearDownForeignThreadGc* =
## With `--mm:arc` a nop.
discard
when not hasThreadSupport:
template setupForeignThreadGc* = discard
template tearDownForeignThreadGc* = discard
elif emulatedThreadVars:
template setupForeignThreadGc* =
{.error: "setupForeignThreadGc is available only when ``--threads:on`` and ``--tlsEmulation:off`` are used".}
template tearDownForeignThreadGc* =
{.error: "tearDownForeignThreadGc is available only when ``--threads:on`` and ``--tlsEmulation:off`` are used".}
elif not hasThreadLocalAllocator:
template setupForeignThreadGc* = discard
template tearDownForeignThreadGc* = discard
proc isObjDisplayCheck(source: PNimTypeV2, targetDepth: int16, token: uint32): bool {.compilerRtl, inl.} =
result = targetDepth <= source.depth and source.display[targetDepth] == token

View File

@@ -19,6 +19,13 @@ when not defined(useNimRtl):
threadType = ThreadType.NimThread
when hasThreadLocalAllocator and not emulatedThreadVars:
proc setupForeignThreadGc*() {.gcsafe, raises: [].} =
initThreadAllocator()
proc tearDownForeignThreadGc*() {.gcsafe, raises: [].} =
releaseThreadAllocator()
when defined(gcDestructors):
proc deallocThreadStorage(p: pointer) = c_free(p)
else:
@@ -83,6 +90,8 @@ else:
deallocThreadStorage(thrd.rawStack)
proc threadProcWrapStackFrame[TArg](thrd: ptr Thread[TArg]) {.raises: [].} =
when hasThreadLocalAllocator:
initThreadAllocator()
when defined(boehmgc):
boehmGC_call_with_stack_base(threadProcWrapDispatch[TArg], thrd)
elif not defined(nogc) and not defined(gogc) and not defined(gcRegions) and not usesDestructors:
@@ -97,6 +106,8 @@ proc threadProcWrapStackFrame[TArg](thrd: ptr Thread[TArg]) {.raises: [].} =
when declared(deallocOsPages): deallocOsPages()
else:
threadProcWrapDispatch(thrd)
when hasThreadLocalAllocator:
releaseThreadAllocator()
template nimThreadProcWrapperBody*(closure: untyped): untyped =
var thrd = cast[ptr Thread[TArg]](closure)

View File

@@ -0,0 +1,59 @@
discard """
matrix: "--mm:arc --threads:on --tlsEmulation:off; --mm:orc --threads:on --tlsEmulation:off"
disabled: "windows"
output: "ok"
timeout: "30"
"""
import std/posix
var
escaped: pointer
reused: pointer
proc allocateOnForeignThread(_: pointer): pointer {.noconv.} =
setupForeignThreadGc()
escaped = allocShared(96)
cast[ptr int](escaped)[] = 73
tearDownForeignThreadGc()
result = nil
proc reuseOnForeignThread(_: pointer): pointer {.noconv.} =
setupForeignThreadGc()
doAssert cast[ptr int](escaped)[] == 73
deallocShared(escaped)
reused = allocShared(96)
doAssert reused == escaped
deallocShared(reused)
tearDownForeignThreadGc()
result = nil
proc consumeDeferredFree(_: pointer): pointer {.noconv.} =
setupForeignThreadGc()
let first = allocShared(96)
let second = allocShared(96)
# The first allocation advances the active chunk and collects its deferred
# foreign frees. The next allocation reuses the remotely returned cell.
doAssert second == escaped
deallocShared(first)
deallocShared(second)
tearDownForeignThreadGc()
result = nil
proc run(worker: proc(_: pointer): pointer {.noconv.}) =
var thread: Pthread
doAssert pthread_create(addr thread, nil, worker, nil) == 0
doAssert pthread_join(thread, nil) == 0
# setup/teardown is the checkout/return boundary. A distinct native thread can
# safely inherit the allocator even while one of its allocations is still live.
run(allocateOnForeignThread)
run(reuseOnForeignThread)
# A free that arrives while the allocator is idle is queued on its handle and
# consumed after that allocator is handed to another foreign thread.
run(allocateOnForeignThread)
deallocShared(escaped)
run(consumeDeferredFree)
echo "ok"

View File

@@ -0,0 +1,64 @@
discard """
matrix: "--mm:arc --threads:on; --mm:orc --threads:on"
output: "ok"
timeout: "30"
"""
import std/[atomics, typedthreads]
const
pointerCount = 512
drainCount = 2048
iterations {.intdefine.} = 200
sizes = [16, 64, 4000, 4096, 8192]
var
pointers: array[pointerCount, pointer]
mayExit: Atomic[bool]
proc owner() {.thread.} =
for i in 0..<pointers.len:
let size = sizes[i mod sizes.len]
pointers[i] = allocShared(size)
cast[ptr byte](pointers[i])[] = byte(i)
proc borrower() {.thread.} =
while not mayExit.load(moAcquire):
discard
proc drain() {.thread.} =
var drained: array[drainCount, pointer]
for i in 0..<drained.len:
drained[i] = allocShared(sizes[i mod sizes.len])
for p in drained:
deallocShared(p)
let occupied = getOccupiedMem()
doAssert occupied == 0, "allocator retained " & $occupied & " bytes"
for _ in 0..<iterations:
# The owner retires with live small and big allocations. The borrower checks
# out that region while this already-running main thread returns the cells.
# This races foreign queue publication against both directions of the
# MemRegion handoff without creating an unbounded number of regions.
block:
var thread: Thread[void]
createThread(thread, owner)
joinThread(thread)
mayExit.store(false, moRelaxed)
var borrowerThread: Thread[void]
createThread(borrowerThread, borrower)
for i, p in pointers:
if i == pointers.len div 2:
# Let the borrower tear the allocator down while the second half of the
# foreign publications are still in flight.
mayExit.store(true, moRelease)
deallocShared(p)
joinThread(borrowerThread)
block:
var thread: Thread[void]
createThread(thread, drain)
joinThread(thread)
echo "ok"

View File

@@ -0,0 +1,92 @@
discard """
matrix: "--mm:arc --threads:on; --mm:orc --threads:on"
output: "ok"
timeout: "30"
"""
import std/[atomics, typedthreads]
const concurrentThreads = 4
var
escaped: pointer
reused: pointer
bigEscaped: pointer
roundAddresses: array[2, array[concurrentThreads, pointer]]
ready: Atomic[int]
mayExit: Atomic[bool]
proc allocateEscaped() {.thread.} =
escaped = allocShared(64)
cast[ptr int](escaped)[] = 42
proc consumeAfterHandoff() {.thread.} =
doAssert cast[ptr int](escaped)[] == 42
deallocShared(escaped)
reused = allocShared(64)
doAssert reused == escaped
deallocShared(reused)
# A live allocation can outlast its original thread. The next thread receives
# the same allocator and its stable handle makes the deallocation local again.
block:
var thread: Thread[void]
createThread(thread, allocateEscaped)
joinThread(thread)
createThread(thread, consumeAfterHandoff)
joinThread(thread)
proc allocateBigEscaped() {.thread.} =
bigEscaped = allocShared(8192)
cast[ptr int](bigEscaped)[] = 91
proc consumeBigAfterHandoff() {.thread.} =
doAssert cast[ptr int](bigEscaped)[] == 91
deallocShared(bigEscaped)
let p = allocShared(8192)
doAssert p == bigEscaped
deallocShared(p)
# Big chunks use a separate deferred-free queue on the stable handle.
block:
var thread: Thread[void]
createThread(thread, allocateBigEscaped)
joinThread(thread)
createThread(thread, consumeBigAfterHandoff)
joinThread(thread)
proc allocateConcurrently(arg: tuple[round, index: int]) {.thread.} =
let p = allocShared(80)
roundAddresses[arg.round][arg.index] = p
deallocShared(p)
discard ready.fetchAdd(1, moRelease)
while not mayExit.load(moAcquire):
discard
proc runConcurrentRound(round: int) =
var threads: array[concurrentThreads, Thread[tuple[round, index: int]]]
ready.store(0, moRelaxed)
mayExit.store(false, moRelaxed)
for i in 0..<threads.len:
createThread(threads[i], allocateConcurrently, (round, i))
while ready.load(moAcquire) != concurrentThreads:
discard
mayExit.store(true, moRelease)
for thread in threads.mitems:
joinThread(thread)
# The first round establishes the peak number of simultaneous allocators. The
# following rounds must reuse those regions instead of reserving one region per
# new thread.
runConcurrentRound(0)
for _ in 0..<32:
runConcurrentRound(1)
for p in roundAddresses[1]:
var found = false
for old in roundAddresses[0]:
if p == old:
found = true
break
doAssert found
echo "ok"

View File

@@ -0,0 +1,56 @@
discard """
matrix: "--mm:arc --threads:on; --mm:orc --threads:on"
output: "ok"
timeout: "30"
"""
import std/[atomics, typedthreads]
const
pointerCount = 1024
iterations = 100
var
pointers: array[pointerCount, pointer]
drainPointers: array[pointerCount, pointer]
ready: Atomic[bool]
start: Atomic[bool]
remoteDone: Atomic[bool]
proc owner() {.thread.} =
for i in 0..<pointers.len:
pointers[i] = allocShared(16 + (i mod 8) * 16)
ready.store(true, moRelease)
while not start.load(moAcquire):
discard
# Race allocator activity against remote frees. The owner can consume cells
# while the remote thread is still publishing entries to its handle.
while not remoteDone.load(moAcquire):
for i in 0..<8:
let p = allocShared(16 + i * 16)
deallocShared(p)
# Exhaust local free lists so all remaining remote lists are consumed before
# this allocator is returned to the pool.
for i in 0..<drainPointers.len:
drainPointers[i] = allocShared(16 + (i mod 8) * 16)
for p in drainPointers:
deallocShared(p)
doAssert getOccupiedMem() == 0
for _ in 0..<iterations:
ready.store(false, moRelaxed)
start.store(false, moRelaxed)
remoteDone.store(false, moRelaxed)
var thread: Thread[void]
createThread(thread, owner)
while not ready.load(moAcquire):
discard
start.store(true, moRelease)
for p in pointers:
deallocShared(p)
remoteDone.store(true, moRelease)
joinThread(thread)
echo "ok"