cycle breaker (#13593)
* cycle breaking as an alternative to cycle detection
This commit is contained in:
parent
705e283fef
commit
35fb38629f
10 changed files with 298 additions and 41 deletions
227
lib/system/cyclebreaker.nim
Normal file
227
lib/system/cyclebreaker.nim
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
#
|
||||
#
|
||||
# Nim's Runtime Library
|
||||
# (c) Copyright 2020 Andreas Rumpf
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
#[
|
||||
A Cycle breaker for Nim
|
||||
-----------------------
|
||||
|
||||
Instead of "collecting" cycles with all of its pitfalls we will break cycles.
|
||||
We exploit that every 'ref' can be 'nil' for this and so get away without
|
||||
a distinction between weak and strong pointers. The required runtime
|
||||
mechanisms are the same though: We need to be able to traverse the graph.
|
||||
This design has the tremendous benefit that it doesn't require a dedicated
|
||||
'rawDispose' operation and that it plays well with Nim's cost model.
|
||||
The cost of freeing a subgraph with cycles is 2 * N rather than N, that's all.
|
||||
|
||||
Cycles do not have to be prepared via .acyclic, there are not multiple
|
||||
pointless traversals, only a single proc, `breakCycles` is exposed as a
|
||||
separate module.
|
||||
|
||||
Algorithm
|
||||
---------
|
||||
|
||||
We traverse the graph and notice the nodes we've already traversed. If we
|
||||
marked the node already, we set the pointer that leads to this node to 'nil'
|
||||
and decrement the reference count of the cell we pointed at.
|
||||
|
||||
We notice that multiple paths to the same object do not mean
|
||||
we found a cycle, it only means the node is shared.
|
||||
|
||||
|
||||
a -------> b <----- c
|
||||
| ^ ^
|
||||
+----------+ |
|
||||
| |
|
||||
+-------------------+
|
||||
|
||||
If we simply remove all links to already processed nodes we end up with:
|
||||
|
||||
a -------> b c
|
||||
| ^
|
||||
+ |
|
||||
| |
|
||||
+-------------------+
|
||||
|
||||
That seems acceptable, no leak is produced. This implies that the standard
|
||||
depth-first traversal suffices.
|
||||
|
||||
]#
|
||||
const
|
||||
colGreen = 0b000
|
||||
colYellow = 0b001
|
||||
colRed = 0b010
|
||||
rcShift = 3 # shift by rcShift to get the reference counter
|
||||
colorMask = 0b011
|
||||
|
||||
type
|
||||
TraceProc = proc (p, env: pointer) {.nimcall, benign.}
|
||||
DisposeProc = proc (p: pointer) {.nimcall, benign.}
|
||||
|
||||
template color(c): untyped = c.rc and colorMask
|
||||
template setColor(c, col) =
|
||||
c.rc = c.rc and not colorMask or col
|
||||
|
||||
proc nimIncRefCyclic(p: pointer) {.compilerRtl, inl.} =
|
||||
let h = head(p)
|
||||
inc h.rc, rcIncrement
|
||||
|
||||
type
|
||||
CellTuple = (ptr pointer, PNimType)
|
||||
CellArray = ptr UncheckedArray[CellTuple]
|
||||
CellSeq = object
|
||||
len, cap: int
|
||||
d: CellArray
|
||||
|
||||
GcEnv = object
|
||||
traceStack: CellSeq
|
||||
|
||||
# ------------------- cell seq handling --------------------------------------
|
||||
|
||||
proc add(s: var CellSeq, c: ptr pointer; t: PNimType) {.inline.} =
|
||||
if s.len >= s.cap:
|
||||
s.cap = s.cap * 3 div 2
|
||||
when defined(useMalloc):
|
||||
var d = cast[CellArray](c_malloc(uint(s.cap * sizeof(CellTuple))))
|
||||
else:
|
||||
var d = cast[CellArray](alloc(s.cap * sizeof(CellTuple)))
|
||||
copyMem(d, s.d, s.len * sizeof(CellTuple))
|
||||
when defined(useMalloc):
|
||||
c_free(s.d)
|
||||
else:
|
||||
dealloc(s.d)
|
||||
s.d = d
|
||||
# XXX: realloc?
|
||||
s.d[s.len] = (c, t)
|
||||
inc(s.len)
|
||||
|
||||
proc init(s: var CellSeq, cap: int = 1024) =
|
||||
s.len = 0
|
||||
s.cap = cap
|
||||
when defined(useMalloc):
|
||||
s.d = cast[CellArray](c_malloc(uint(s.cap * sizeof(CellTuple))))
|
||||
else:
|
||||
s.d = cast[CellArray](alloc(s.cap * sizeof(CellTuple)))
|
||||
|
||||
proc deinit(s: var CellSeq) =
|
||||
when defined(useMalloc):
|
||||
c_free(s.d)
|
||||
else:
|
||||
dealloc(s.d)
|
||||
s.d = nil
|
||||
s.len = 0
|
||||
s.cap = 0
|
||||
|
||||
proc pop(s: var CellSeq): (ptr pointer, PNimType) =
|
||||
result = s.d[s.len-1]
|
||||
dec s.len
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
proc trace(p: pointer; desc: PNimType; j: var GcEnv) {.inline.} =
|
||||
when false:
|
||||
cprintf("[Trace] desc: %p %p\n", desc, p)
|
||||
cprintf("[Trace] trace: %p\n", desc.traceImpl)
|
||||
if desc.traceImpl != nil:
|
||||
cast[TraceProc](desc.traceImpl)(p, addr(j))
|
||||
|
||||
proc nimTraceRef(q: pointer; desc: PNimType; env: pointer) {.compilerRtl.} =
|
||||
let p = cast[ptr pointer](q)
|
||||
when traceCollector:
|
||||
cprintf("[Trace] raw: %p\n", p)
|
||||
cprintf("[Trace] deref: %p\n", p[])
|
||||
if p[] != nil:
|
||||
var j = cast[ptr GcEnv](env)
|
||||
j.traceStack.add(p, desc)
|
||||
|
||||
proc nimTraceRefDyn(q: pointer; env: pointer) {.compilerRtl.} =
|
||||
let p = cast[ptr pointer](q)
|
||||
when traceCollector:
|
||||
cprintf("[TraceDyn] raw: %p\n", p)
|
||||
cprintf("[TraceDyn] deref: %p\n", p[])
|
||||
if p[] != nil:
|
||||
var j = cast[ptr GcEnv](env)
|
||||
j.traceStack.add(p, cast[ptr PNimType](p[])[])
|
||||
|
||||
var markerGeneration: int
|
||||
|
||||
proc breakCycles(s: Cell; desc: PNimType) =
|
||||
let markerColor = if (markerGeneration and 1) == 0: colRed
|
||||
else: colYellow
|
||||
atomicInc markerGeneration
|
||||
when traceCollector:
|
||||
cprintf("[BreakCycles] starting: %p %s RC %ld trace proc %p\n",
|
||||
s, desc.name, s.rc shr rcShift, desc.traceImpl)
|
||||
|
||||
var j: GcEnv
|
||||
init j.traceStack
|
||||
s.setColor markerColor
|
||||
trace(s +! sizeof(RefHeader), desc, j)
|
||||
|
||||
while j.traceStack.len > 0:
|
||||
let (u, desc) = j.traceStack.pop()
|
||||
let p = u[]
|
||||
let t = head(p)
|
||||
if t.color != markerColor:
|
||||
t.setColor markerColor
|
||||
trace(p, desc, j)
|
||||
when traceCollector:
|
||||
cprintf("[BreakCycles] followed: %p RC %ld\n", t, t.rc shr rcShift)
|
||||
else:
|
||||
if (t.rc shr rcShift) > 0:
|
||||
dec t.rc, rcIncrement
|
||||
# mark as a link that the produced destructor does not have to follow:
|
||||
u[] = nil
|
||||
when traceCollector:
|
||||
cprintf("[BreakCycles] niled out: %p RC %ld\n", t, t.rc shr rcShift)
|
||||
else:
|
||||
# anyhow as a link that the produced destructor does not have to follow:
|
||||
u[] = nil
|
||||
cprintf("[Bug] %p %s RC %ld\n", t, desc.name, t.rc shr rcShift)
|
||||
deinit j.traceStack
|
||||
|
||||
proc thinout*[T](x: ref T) {.inline.} =
|
||||
## turn the subgraph starting with `x` into its spanning tree by
|
||||
## `nil`'ing out any pointers that would harm the spanning tree
|
||||
## structure. Any back pointers that introduced cycles
|
||||
## and thus would keep the graph from being freed are `nil`'ed.
|
||||
## This is a form of cycle collection that works well with Nim's ARC
|
||||
## and its associated cost model.
|
||||
proc getDynamicTypeInfo[T](x: T): PNimType {.magic: "GetTypeInfo", noSideEffect, locks: 0.}
|
||||
|
||||
breakCycles(head(cast[pointer](x)), getDynamicTypeInfo(x[]))
|
||||
|
||||
proc thinout*[T: proc](x: T) {.inline.} =
|
||||
proc rawEnv[T: proc](x: T): pointer {.noSideEffect, inline.} =
|
||||
{.emit: """
|
||||
`result` = `x`.ClE_0;
|
||||
""".}
|
||||
|
||||
let p = rawEnv(x)
|
||||
breakCycles(head(p), cast[ptr PNimType](p)[])
|
||||
|
||||
proc nimDecRefIsLastCyclicDyn(p: pointer): bool {.compilerRtl, inl.} =
|
||||
if p != nil:
|
||||
var cell = head(p)
|
||||
if (cell.rc and not rcMask) == 0:
|
||||
result = true
|
||||
#cprintf("[DESTROY] %p\n", p)
|
||||
else:
|
||||
dec cell.rc, rcIncrement
|
||||
# According to Lins it's correct to do nothing else here.
|
||||
#cprintf("[DeCREF] %p\n", p)
|
||||
|
||||
proc nimDecRefIsLastCyclicStatic(p: pointer; desc: PNimType): bool {.compilerRtl, inl.} =
|
||||
if p != nil:
|
||||
var cell = head(p)
|
||||
if (cell.rc and not rcMask) == 0:
|
||||
result = true
|
||||
#cprintf("[DESTROY] %p %s\n", p, desc.name)
|
||||
else:
|
||||
dec cell.rc, rcIncrement
|
||||
#cprintf("[DeCREF] %p %s %ld\n", p, desc.name, cell.rc)
|
||||
Loading…
Add table
Add a link
Reference in a new issue