Merge branch 'gc-upgrade' into compiler-refactor
This commit is contained in:
commit
47ac1be6aa
|
@ -71,7 +71,6 @@ type
|
||||||
cycles: int
|
cycles: int
|
||||||
nextGC: int
|
nextGC: int
|
||||||
pointers: HashSet[uint64]
|
pointers: HashSet[uint64]
|
||||||
objects: seq[ptr HeapObject]
|
|
||||||
PeonVM* = object
|
PeonVM* = object
|
||||||
## The Peon Virtual Machine.
|
## The Peon Virtual Machine.
|
||||||
## Note how the only data
|
## Note how the only data
|
||||||
|
@ -105,7 +104,6 @@ proc newPeonGC*: PeonGC =
|
||||||
## Initializes a new, blank
|
## Initializes a new, blank
|
||||||
## garbage collector
|
## garbage collector
|
||||||
result.bytesAllocated = (0, 0)
|
result.bytesAllocated = (0, 0)
|
||||||
result.objects = @[]
|
|
||||||
result.nextGC = FirstGC
|
result.nextGC = FirstGC
|
||||||
result.cycles = 0
|
result.cycles = 0
|
||||||
|
|
||||||
|
@ -113,9 +111,19 @@ proc newPeonGC*: PeonGC =
|
||||||
proc collect*(self: var PeonVM)
|
proc collect*(self: var PeonVM)
|
||||||
|
|
||||||
|
|
||||||
|
# Our pointer tagging routines
|
||||||
|
template tag(p: untyped): untyped = cast[pointer](cast[uint64](p) or (1'u64 shl 63'u64))
|
||||||
|
template untag(p: untyped): untyped = cast[pointer](cast[uint64](p) and 0x7fffffffffffffff'u64)
|
||||||
|
template getTag(p: untyped): untyped = (p and (1'u64 shl 63'u64)) == 0
|
||||||
|
|
||||||
|
|
||||||
proc reallocate*(self: var PeonVM, p: pointer, oldSize: int, newSize: int): pointer =
|
proc reallocate*(self: var PeonVM, p: pointer, oldSize: int, newSize: int): pointer =
|
||||||
## Simple wrapper around realloc with
|
## Simple wrapper around realloc with
|
||||||
## built-in garbage collection
|
## built-in garbage collection. Callers
|
||||||
|
## should keep in mind that the returned
|
||||||
|
## pointer is tagged (bit 63 is set to 1)
|
||||||
|
## and should be passed to untag() before
|
||||||
|
## being dereferenced or otherwise used
|
||||||
self.gc.bytesAllocated.current += newSize - oldSize
|
self.gc.bytesAllocated.current += newSize - oldSize
|
||||||
try:
|
try:
|
||||||
when debugMem:
|
when debugMem:
|
||||||
|
@ -137,9 +145,9 @@ proc reallocate*(self: var PeonVM, p: pointer, oldSize: int, newSize: int): poin
|
||||||
when debugStressGC:
|
when debugStressGC:
|
||||||
self.collect()
|
self.collect()
|
||||||
else:
|
else:
|
||||||
if self.gc.bytesAllocated.current > self.gc.nextGC:
|
if self.gc.bytesAllocated.current >= self.gc.nextGC:
|
||||||
self.collect()
|
self.collect()
|
||||||
result = realloc(p, newSize)
|
result = tag(realloc(untag(p), newSize))
|
||||||
except NilAccessDefect:
|
except NilAccessDefect:
|
||||||
stderr.writeLine("Peon: could not manage memory, segmentation fault")
|
stderr.writeLine("Peon: could not manage memory, segmentation fault")
|
||||||
quit(139) # For now, there's not much we can do if we can't get the memory we need, so we exit
|
quit(139) # For now, there's not much we can do if we can't get the memory we need, so we exit
|
||||||
|
@ -167,17 +175,18 @@ template setKind[T, K](t: var T, kind: untyped, target: K) =
|
||||||
|
|
||||||
|
|
||||||
proc allocate(self: var PeonVM, kind: ObjectKind, size: typedesc, count: int): ptr HeapObject {.inline.} =
|
proc allocate(self: var PeonVM, kind: ObjectKind, size: typedesc, count: int): ptr HeapObject {.inline.} =
|
||||||
## Allocates an object on the heap
|
## Allocates an object on the heap and adds its
|
||||||
result = cast[ptr HeapObject](self.reallocate(nil, 0, sizeof(HeapObject)))
|
## location to the internal pointer list of the
|
||||||
|
## garbage collector
|
||||||
|
result = cast[ptr HeapObject](untag(self.reallocate(nil, 0, sizeof(HeapObject))))
|
||||||
setkind(result[], kind, kind)
|
setkind(result[], kind, kind)
|
||||||
result.marked = false
|
result.marked = false
|
||||||
case kind:
|
case kind:
|
||||||
of String:
|
of String:
|
||||||
result.str = cast[ptr UncheckedArray[char]](self.reallocate(nil, 0, sizeof(size) * count))
|
result.str = cast[ptr UncheckedArray[char]](untag(self.reallocate(nil, 0, sizeof(size) * count)))
|
||||||
result.len = count
|
result.len = count
|
||||||
else:
|
else:
|
||||||
discard # TODO
|
discard # TODO
|
||||||
self.gc.objects.add(result)
|
|
||||||
self.gc.pointers.incl(cast[uint64](result))
|
self.gc.pointers.incl(cast[uint64](result))
|
||||||
when debugAlloc:
|
when debugAlloc:
|
||||||
echo &"DEBUG - GC: Allocated new object: {result[]}"
|
echo &"DEBUG - GC: Allocated new object: {result[]}"
|
||||||
|
@ -195,54 +204,50 @@ proc mark(self: ptr HeapObject): bool =
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
proc markRoots(self: var PeonVM): seq[ptr HeapObject] =
|
proc markRoots(self: var PeonVM): HashSet[ptr HeapObject] =
|
||||||
## Marks root objects *not* to be
|
## Marks root objects *not* to be
|
||||||
## collected by the GC and returns
|
## collected by the GC and returns
|
||||||
## their addresses
|
## their addresses
|
||||||
when debugGC:
|
when debugGC:
|
||||||
echo "DEBUG - GC: Starting mark phase"
|
echo "DEBUG - GC: Starting mark phase"
|
||||||
# Unlike what bob does in his book,
|
# Unlike what Bob does in his book, we keep track
|
||||||
# we keep track of objects in a different
|
# of objects another way, mainly due to the difference
|
||||||
# way due to the difference of our design.
|
# of our respective designs. Specifically, our VM only
|
||||||
# Specifically, we don't have neat structs for
|
# handles a single type (uint64), while Lox has a stack
|
||||||
# all peon objects: When we allocate() an object,
|
# of heap-allocated structs (which is convenient, but slow).
|
||||||
# we keep track of the small wrapper it created
|
# The previous implementation would just store all pointers
|
||||||
# along with its type and other metadata. Then,
|
# allocated by us in a hash set and then check if any source
|
||||||
# we can go through the various sources of roots
|
# of roots contained any of the integer values that it was
|
||||||
# in the VM, see if they match any pointers we
|
# keeping track of, but this meant that if a primitive object's
|
||||||
# already know about (we store them in a hash set so
|
# value happened to collide with an active pointer the GC would
|
||||||
# it's really fast), and then we can be sure that
|
# mistakenly assume the object was reachable, potentially leading
|
||||||
# anything that's in the difference (i.e. mathematical
|
# to a nasty memory leak. The current implementation uses pointer
|
||||||
# set difference) between our full list of pointers
|
# tagging: we know that modern CPUs never use bit 63 in addresses,
|
||||||
# and the live ones is not a root object, so if it's
|
# so if it set we know it cannot be a pointer, and if it is set we
|
||||||
# not indirectly reachable through a root itself, it
|
# just need to check if it's in our list of active addresses or not.
|
||||||
# can be freed. I'm not sure if I can call this GC
|
# This should resolve the potential memory leak (hopefully)
|
||||||
# strategy precise, since technically there is a chance
|
var result = initHashSet[uint64](self.gc.pointers.len())
|
||||||
# for a regular value to collide with one of the pointers
|
|
||||||
# we allocated and that would cause a memory leak, but
|
|
||||||
# with a 64-bit address-space it probably hardly matters,
|
|
||||||
# so I guess this is a mostly-precise Mark&Sweep collector
|
|
||||||
var live = initHashSet[uint64](self.gc.pointers.len())
|
|
||||||
for obj in self.calls:
|
for obj in self.calls:
|
||||||
|
if not obj.getTag():
|
||||||
|
continue
|
||||||
if obj in self.gc.pointers:
|
if obj in self.gc.pointers:
|
||||||
live.incl(obj)
|
result.incl(obj)
|
||||||
for obj in self.operands:
|
for obj in self.operands:
|
||||||
|
if not obj.getTag():
|
||||||
|
continue
|
||||||
if obj in self.gc.pointers:
|
if obj in self.gc.pointers:
|
||||||
live.incl(obj)
|
result.incl(obj)
|
||||||
# We preallocate the space on the seq
|
|
||||||
result = newSeqOfCap[ptr HeapObject](len(live))
|
|
||||||
var obj: ptr HeapObject
|
var obj: ptr HeapObject
|
||||||
for p in live:
|
for p in result:
|
||||||
obj = cast[ptr HeapObject](p)
|
obj = cast[ptr HeapObject](p)
|
||||||
if obj.mark():
|
if obj.mark():
|
||||||
result.add(obj)
|
when debugMarkGC:
|
||||||
when debugGC:
|
|
||||||
echo &"DEBUG - GC: Marked object: {obj[]}"
|
echo &"DEBUG - GC: Marked object: {obj[]}"
|
||||||
when debugGC:
|
when debugGC:
|
||||||
echo "DEBUG - GC: Mark phase complete"
|
echo "DEBUG - GC: Mark phase complete"
|
||||||
|
|
||||||
|
|
||||||
proc trace(self: var PeonVM, roots: seq[ptr HeapObject]) =
|
proc trace(self: var PeonVM, roots: HashSet[ptr HeapObject]) =
|
||||||
## Traces references to other
|
## Traces references to other
|
||||||
## objects starting from the
|
## objects starting from the
|
||||||
## roots. The second argument
|
## roots. The second argument
|
||||||
|
@ -252,7 +257,8 @@ proc trace(self: var PeonVM, roots: seq[ptr HeapObject]) =
|
||||||
## this is where we blacken gray
|
## this is where we blacken gray
|
||||||
## objects
|
## objects
|
||||||
when debugGC:
|
when debugGC:
|
||||||
echo &"DEBUG - GC: Tracing indirect references from {len(roots)} roots"
|
if len(roots) > 0:
|
||||||
|
echo &"DEBUG - GC: Tracing indirect references from {len(roots)} root{(if len(roots) > 1: \"s\" else: \"\")}"
|
||||||
var count = 0
|
var count = 0
|
||||||
for root in roots:
|
for root in roots:
|
||||||
case root.kind:
|
case root.kind:
|
||||||
|
@ -261,13 +267,16 @@ proc trace(self: var PeonVM, roots: seq[ptr HeapObject]) =
|
||||||
else:
|
else:
|
||||||
discard # TODO: Other types
|
discard # TODO: Other types
|
||||||
when debugGC:
|
when debugGC:
|
||||||
echo &"DEBUG - GC: Traced {count} indirect references"
|
echo &"DEBUG - GC: Traced {count} indirect reference{(if count != 1: \"s\" else: \"\")}"
|
||||||
|
|
||||||
|
|
||||||
proc free(self: var PeonVM, obj: ptr HeapObject) =
|
proc free(self: var PeonVM, obj: ptr HeapObject) =
|
||||||
## Frees a single heap-allocated
|
## Frees a single heap-allocated
|
||||||
## peon object and all the memory
|
## peon object and all the memory
|
||||||
## it directly or indirectly owns
|
## it directly or indirectly owns. Note
|
||||||
|
## that the pointer itself is not released
|
||||||
|
## from the GC's internal table and must be
|
||||||
|
## handled by the caller
|
||||||
when debugAlloc:
|
when debugAlloc:
|
||||||
echo &"DEBUG - GC: Freeing object: {obj[]}"
|
echo &"DEBUG - GC: Freeing object: {obj[]}"
|
||||||
case obj.kind:
|
case obj.kind:
|
||||||
|
@ -279,7 +288,6 @@ proc free(self: var PeonVM, obj: ptr HeapObject) =
|
||||||
else:
|
else:
|
||||||
discard # TODO
|
discard # TODO
|
||||||
self.free(HeapObject, obj)
|
self.free(HeapObject, obj)
|
||||||
self.gc.pointers.excl(cast[uint64](obj))
|
|
||||||
when debugAlloc:
|
when debugAlloc:
|
||||||
echo &"DEBUG - GC: Current heap size: {self.gc.bytesAllocated.current}"
|
echo &"DEBUG - GC: Current heap size: {self.gc.bytesAllocated.current}"
|
||||||
echo &"DEBUG - GC: Total bytes allocated: {self.gc.bytesAllocated.total}"
|
echo &"DEBUG - GC: Total bytes allocated: {self.gc.bytesAllocated.total}"
|
||||||
|
@ -291,37 +299,32 @@ proc sweep(self: var PeonVM) =
|
||||||
## Sweeps unmarked objects
|
## Sweeps unmarked objects
|
||||||
## that have been left behind
|
## that have been left behind
|
||||||
## during the mark phase.
|
## during the mark phase.
|
||||||
## This is more convoluted
|
|
||||||
## than it needs to be because
|
|
||||||
## nim disallows changing the
|
|
||||||
## size of a sequence during
|
|
||||||
## iteration
|
|
||||||
when debugGC:
|
when debugGC:
|
||||||
echo "DEBUG - GC: Beginning sweeping phase"
|
echo "DEBUG - GC: Beginning sweeping phase"
|
||||||
var j = -1
|
|
||||||
var idx = 0
|
|
||||||
when debugGC:
|
when debugGC:
|
||||||
var count = 0
|
var count = 0
|
||||||
while j < self.gc.objects.high():
|
var current: ptr HeapObject
|
||||||
inc(j)
|
var freed: HashSet[uint64]
|
||||||
if self.gc.objects[j].marked:
|
for p in self.gc.pointers:
|
||||||
|
current = cast[ptr HeapObject](p)
|
||||||
|
if current.marked:
|
||||||
# Object is marked: don't touch it,
|
# Object is marked: don't touch it,
|
||||||
# but reset its mark so that it doesn't
|
# but reset its mark so that it doesn't
|
||||||
# stay alive forever
|
# stay alive forever
|
||||||
when debugGC:
|
when debugMarkGC:
|
||||||
echo &"DEBUG - GC: Unmarking object: {self.gc.objects[j][]}"
|
echo &"DEBUG - GC: Unmarking object: {current[]}"
|
||||||
self.gc.objects[j].marked = false
|
current.marked = false
|
||||||
inc(idx)
|
|
||||||
else:
|
else:
|
||||||
# Object is unmarked: its memory is
|
# Object is unmarked: its memory is
|
||||||
# fair game
|
# fair game
|
||||||
self.free(self.gc.objects[idx])
|
self.free(current)
|
||||||
self.gc.objects.delete(idx)
|
freed.incl(p)
|
||||||
inc(idx)
|
|
||||||
when debugGC:
|
when debugGC:
|
||||||
inc(count)
|
inc(count)
|
||||||
|
# Set difference
|
||||||
|
self.gc.pointers = self.gc.pointers - freed
|
||||||
when debugGC:
|
when debugGC:
|
||||||
echo &"DEBUG - GC: Swept {count} objects"
|
echo &"DEBUG - GC: Swept {count} object{(if count > 1: \"s\" else: \"\")}"
|
||||||
|
|
||||||
|
|
||||||
proc collect(self: var PeonVM) =
|
proc collect(self: var PeonVM) =
|
||||||
|
@ -331,6 +334,7 @@ proc collect(self: var PeonVM) =
|
||||||
when debugGC:
|
when debugGC:
|
||||||
let before = self.gc.bytesAllocated.current
|
let before = self.gc.bytesAllocated.current
|
||||||
let time = getMonoTime().ticks().float() / 1_000_000
|
let time = getMonoTime().ticks().float() / 1_000_000
|
||||||
|
echo ""
|
||||||
echo &"DEBUG - GC: Starting collection cycle at heap size {self.gc.bytesAllocated.current}"
|
echo &"DEBUG - GC: Starting collection cycle at heap size {self.gc.bytesAllocated.current}"
|
||||||
echo &"DEBUG - GC: Total bytes allocated: {self.gc.bytesAllocated.total}"
|
echo &"DEBUG - GC: Total bytes allocated: {self.gc.bytesAllocated.total}"
|
||||||
echo &"DEBUG - GC: Tracked objects: {self.gc.pointers.len()}"
|
echo &"DEBUG - GC: Tracked objects: {self.gc.pointers.len()}"
|
||||||
|
@ -339,6 +343,8 @@ proc collect(self: var PeonVM) =
|
||||||
self.trace(self.markRoots())
|
self.trace(self.markRoots())
|
||||||
self.sweep()
|
self.sweep()
|
||||||
self.gc.nextGC = self.gc.bytesAllocated.current * HeapGrowFactor
|
self.gc.nextGC = self.gc.bytesAllocated.current * HeapGrowFactor
|
||||||
|
if self.gc.nextGC == 0:
|
||||||
|
self.gc.nextGC = FirstGC
|
||||||
when debugGC:
|
when debugGC:
|
||||||
echo &"DEBUG - GC: Collection cycle has terminated in {getMonoTime().ticks().float() / 1_000_000 - time:.2f} ms, collected {before - self.gc.bytesAllocated.current} bytes of memory in total"
|
echo &"DEBUG - GC: Collection cycle has terminated in {getMonoTime().ticks().float() / 1_000_000 - time:.2f} ms, collected {before - self.gc.bytesAllocated.current} bytes of memory in total"
|
||||||
echo &"DEBUG - GC: Next cycle at {self.gc.nextGC} bytes"
|
echo &"DEBUG - GC: Next cycle at {self.gc.nextGC} bytes"
|
||||||
|
|
|
@ -17,13 +17,14 @@ import strformat
|
||||||
# These variables can be tweaked to debug and test various components of the toolchain
|
# These variables can be tweaked to debug and test various components of the toolchain
|
||||||
const debugLexer* {.booldefine.} = false # Print the tokenizer's output
|
const debugLexer* {.booldefine.} = false # Print the tokenizer's output
|
||||||
const debugParser* {.booldefine.} = false # Print the AST generated by the parser
|
const debugParser* {.booldefine.} = false # Print the AST generated by the parser
|
||||||
const debugCompiler* {.booldefine.} = false # Disassemble and /or print the code generated by the compiler
|
const debugCompiler* {.booldefine.} = false # Disassemble and/or print the code generated by the compiler
|
||||||
const debugVM* {.booldefine.} = false # Enable the runtime debugger in the bytecode VM
|
const debugVM* {.booldefine.} = false # Enable the runtime debugger in the bytecode VM
|
||||||
const debugGC* {.booldefine.} = false # Debug the Garbage Collector (extremely verbose)
|
const debugGC* {.booldefine.} = false # Debug the Garbage Collector (extremely verbose)
|
||||||
const debugAlloc* {.booldefine.} = false # Trace object allocation (extremely verbose)
|
const debugAlloc* {.booldefine.} = false # Trace object allocation (extremely verbose)
|
||||||
const debugMem* {.booldefine.} = false # Debug the memory allocator (extremely verbose)
|
const debugMem* {.booldefine.} = false # Debug the memory allocator (extremely verbose)
|
||||||
const debugSerializer* {.booldefine.} = false # Validate the bytecode serializer's output
|
const debugSerializer* {.booldefine.} = false # Validate the bytecode serializer's output
|
||||||
const debugStressGC* {.booldefine.} = false # Make the GC run a collection at every allocation (VERY SLOW!)
|
const debugStressGC* {.booldefine.} = false # Make the GC run a collection at every allocation (VERY SLOW!)
|
||||||
|
const debugMarkGC* {.booldefine.} = false # Trace the marking phase object by object (extremely verbose)
|
||||||
const PeonBytecodeMarker* = "PEON_BYTECODE" # Magic value at the beginning of bytecode files
|
const PeonBytecodeMarker* = "PEON_BYTECODE" # Magic value at the beginning of bytecode files
|
||||||
const HeapGrowFactor* = 2 # The growth factor used by the GC to schedule the next collection
|
const HeapGrowFactor* = 2 # The growth factor used by the GC to schedule the next collection
|
||||||
const FirstGC* = 1024 * 1024; # How many bytes to allocate before running the first GC
|
const FirstGC* = 1024 * 1024; # How many bytes to allocate before running the first GC
|
||||||
|
|
25
tests/gc.pn
25
tests/gc.pn
|
@ -1,17 +1,24 @@
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
|
|
||||||
var x = 10000000;
|
const max = 50000;
|
||||||
var y = "just a test";
|
|
||||||
print(y);
|
var x = max;
|
||||||
|
var s = "just a test";
|
||||||
|
print(s);
|
||||||
print("Starting GC torture test");
|
print("Starting GC torture test");
|
||||||
print(x);
|
print(x);
|
||||||
while x > 0 {
|
while x > 0 {
|
||||||
"hello";
|
"1";
|
||||||
x = x - 1;
|
x = x - 1;
|
||||||
}
|
}
|
||||||
print("END");
|
x = max;
|
||||||
print(y);
|
print(x);
|
||||||
y = "test";
|
while x > 0 {
|
||||||
print(y);
|
"1";
|
||||||
"";
|
x = x - 1;
|
||||||
|
}
|
||||||
|
print(s);
|
||||||
|
s = "test";
|
||||||
|
print(s);
|
||||||
|
"end of the world";
|
||||||
|
|
Loading…
Reference in New Issue