mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-18 21:01:38 +00:00
Memregion pool no handle (#26110)
Co-authored-by: SirOlaf <34164198+SirOlaf@users.noreply.github.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
59
tests/threads/tthreadallocatorforeignpool.nim
Normal file
59
tests/threads/tthreadallocatorforeignpool.nim
Normal 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"
|
||||
64
tests/threads/tthreadallocatorhandoffrace.nim
Normal file
64
tests/threads/tthreadallocatorhandoffrace.nim
Normal 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"
|
||||
92
tests/threads/tthreadallocatorpool.nim
Normal file
92
tests/threads/tthreadallocatorpool.nim
Normal 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"
|
||||
56
tests/threads/tthreadallocatorpoolrace.nim
Normal file
56
tests/threads/tthreadallocatorpoolrace.nim
Normal 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"
|
||||
Reference in New Issue
Block a user