mirror of
https://github.com/nim-lang/Nim.git
synced 2026-08-04 06:28:38 +00:00
114 lines
4.6 KiB
Nim
114 lines
4.6 KiB
Nim
discard """
|
|
output: '''true peak memory: true'''
|
|
cmd: "nim c --mm:orc -d:release --threads:on $file"
|
|
"""
|
|
|
|
## torcbench (tests/arc/torcbench.nim), threaded — plus the smallest changes
|
|
## that let the generational scheme show up in the number.
|
|
##
|
|
## Each thread runs its own private copy of the torcbench workload: one long
|
|
## doubly-linked list of strings, and a stream of short-lived cyclic trees
|
|
## whose every node embeds a copy of the list header — that is a reference
|
|
## into the list per tree node. Nothing is shared between threads, so the same
|
|
## program is a fair measurement under --mm:orc and --mm:yrc.
|
|
##
|
|
## Two changes vs torcbench, each needed to make the young -> old pattern
|
|
## measurable rather than incidental:
|
|
##
|
|
## 1. The list is built once per THREAD, not once per outer iteration, so it
|
|
## survives long enough to be promoted. It is the old generation; the trees
|
|
## are the young one.
|
|
## 2. Collection runs at a fixed cadence (GC_partialCollect per outer
|
|
## iteration) instead of being left to each collector's threshold
|
|
## heuristic. Without this the benchmark measures how often each collector
|
|
## decides to collect rather than what a collection over this heap costs —
|
|
## and ORC's threshold scales with heap size, so a bigger list makes it
|
|
## collect LESS and the re-trace it is supposed to be paying never appears.
|
|
##
|
|
## Nothing seeds the promotion: the tree stream itself is what ages the list,
|
|
## which is why the workload can stay an ordinary one. `DoublyLinkedNode.prev`
|
|
## and `DoublyLinkedList.tail` are `{.cursor.}`, so copying a `parent` header
|
|
## incRefs `head` alone, and the list is a chain of one-node SCCs rather than
|
|
## a single big one. Dirtiness is per-SCC, so the churn only ever dirties
|
|
## `head`; every node behind it is traced clean by the collections the trees
|
|
## trigger anyway and promotes after YrcPromoteAge of them. The capture then
|
|
## prunes one edge in — at `head.next` — instead of walking 60000 nodes.
|
|
##
|
|
## --mm:orc every collection follows `parent` into the list and re-traces
|
|
## all ListLen nodes of it.
|
|
## --mm:yrc once the list is promoted, capture prunes at the epoch-stamp
|
|
## boundary and walks only the young frontier.
|
|
##
|
|
## Sizing matters: the win is the ratio of old-generation size to young work
|
|
## per collection, so ListLen is large and TreeIters small. Total young work
|
|
## is about the same as torcbench's (200x51 trees vs 25x401). NumThreads=4 is
|
|
## the default because 8 threads on a 4-performance-core machine dilutes the
|
|
## result (1.15x vs 1.69x measured on an M1).
|
|
##
|
|
## nim c -r --mm:orc -d:release --threads:on yrcbech.nim
|
|
## nim c -r --mm:yrc -d:release --threads:on yrcbech.nim
|
|
##
|
|
## Add -d:yrcBenchTime for a wall-clock line, -d:nimOrcStats for capture and
|
|
## prune counts (YRC only).
|
|
|
|
import std/[lists, monotimes, times]
|
|
|
|
const
|
|
NumThreads {.intdefine.} = 4
|
|
OuterIters {.intdefine.} = 200 ## per thread; also the collection count
|
|
ListLen {.intdefine.} = 60000 ## the old generation
|
|
TreeIters {.intdefine.} = 50 ## young trees per collection
|
|
TreeDepth {.intdefine.} = 8
|
|
|
|
type
|
|
Node = ref object
|
|
parent: DoublyLinkedList[string] ## copy of the header: a ref into the list
|
|
le, ri: Node
|
|
self: Node ## self-cycle, forces cycle detection
|
|
|
|
proc buildTree(parent: DoublyLinkedList[string]; depth: int): Node =
|
|
if depth == 0:
|
|
result = nil
|
|
elif depth == 1:
|
|
result = Node(parent: parent)
|
|
result.self = result
|
|
else:
|
|
result = Node(parent: parent,
|
|
le: buildTree(parent, depth - 1),
|
|
ri: buildTree(parent, depth - 2))
|
|
result.self = result
|
|
|
|
proc threadWork() {.thread.} =
|
|
# (1) built once per thread: the old generation
|
|
var leakList = initDoublyLinkedList[string]()
|
|
for j in 1 .. ListLen:
|
|
leakList.append(newString(200))
|
|
|
|
for i in 1 .. OuterIters:
|
|
for k in 0 .. TreeIters:
|
|
discard buildTree(leakList, TreeDepth) # young: dead the moment it returns
|
|
GC_partialCollect(0) # (2) fixed cadence
|
|
|
|
var threads: array[NumThreads, Thread[void]]
|
|
|
|
let t0 = getMonoTime()
|
|
for i in 0 ..< NumThreads:
|
|
createThread(threads[i], threadWork)
|
|
joinThreads(threads)
|
|
GC_fullCollect()
|
|
let dtMs = inMilliseconds(getMonoTime() - t0)
|
|
|
|
when defined(yrcBenchTime):
|
|
echo "wall_ms ", dtMs
|
|
|
|
when not defined(useMalloc):
|
|
echo getOccupiedMem() < 10 * 1024 * 1024, " peak memory: ",
|
|
getMaxMem() < 256 * 1024 * 1024
|
|
else:
|
|
echo "true peak memory: true"
|
|
|
|
when defined(nimOrcStats) and defined(gcYrc):
|
|
let s = GC_orcStats()
|
|
echo "capTotal ", s.capTotal, " capPruned ", s.capPruned,
|
|
" capRepeat ", s.capRepeat
|