Skip to content

Commit

Permalink
Simplify freeObject. Add object destructor docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
fubark committed May 26, 2024
1 parent bf0e7a1 commit fcd6d95
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 309 deletions.
13 changes: 13 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,19 @@ pub fn build(b: *std.Build) !void {
main_step.dependOn(&run.step);
}

{
const main_step = b.step("trace-test", "Run trace tests.");

var opts = getDefaultOptions();
opts.trackGlobalRc = true;
opts.applyOverrides();

const step = try addTraceTest(b, opts);
const run = b.addRunArtifact(step);
run.has_side_effects = no_cache;
main_step.dependOn(&run.step);
}

{
const main_step = b.step("test", "Run all tests.");

Expand Down
13 changes: 12 additions & 1 deletion docs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3470,6 +3470,7 @@ void myNodeFinalizer(CLVM* vm, void* obj) {
* [ARC.](#arc)
* [Reference counting.](#reference-counting)
* [Object destructor.](#object-destructor)
* [Optimizations.](#optimizations)
* [Closures.](#closures-1)
* [Fibers.](#fibers-1)
Expand All @@ -3490,7 +3491,17 @@ In Cyber, there are [primitive and object](#basic-types) values. Primitives don'
Objects are managed by ARC. Each object has its own reference counter. Upon creating a new object, it receives a reference count of 1. When the object is copied, it's **retained** and the reference count increments by 1. When an object value is removed from it's parent or is no longer reachable in the current stack frame, it is **released** and the reference count decrements by 1.
Once the reference count reaches 0 the object begins its destruction procedure. First, child references are released thereby decrementing their reference counts by 1. If the object is a host object, it will invoke its `finalizer` function. Afterwards, the object is freed from memory.
Once the reference count reaches 0 the object begins its [destruction](#object-destructor) procedure.
### Object destructor.
An object's destructor invoked from ARC performs the following in order:
1. Release child references thereby decrementing their reference counts by 1. If any child reference counts reach 0, their destructors are invoked.
2. If the object has a finalizer, it's invoked.
3. The object is freed from memory.
If the destructor is invoked by the GC instead of ARC, cyclable child references are not released in step 1.
Since objects freed by the GC either belongs to a reference cycle or branched from one, the GC will still end up invoking the destructor of all unreachable objects.
This implies that the destructor order is not reliable, but destructors are guaranteed to be invoked for all unreachable objects.
### Optimizations.
The compiler can reduce the number of retain/release ops since it can infer value types even though they are dynamically typed to the user. Arguments passed to functions are only retained depending on the analysis from the callsite.
Expand Down
87 changes: 35 additions & 52 deletions src/arc.zig
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn release(vm: *cy.VM, val: cy.Value) void {
}
if (obj.head.rc == 0) {
// Free children and the object.
@call(.never_inline, cy.heap.freeObject, .{vm, obj, true, false, true});
@call(.never_inline, cy.heap.freeObject, .{vm, obj, false});

if (cy.Trace) {
if (vm.countFrees) {
Expand Down Expand Up @@ -97,12 +97,7 @@ pub fn releaseObject(vm: *cy.VM, obj: *cy.HeapObject) void {
vm.c.trace.numReleaseAttempts += 1;
}
if (obj.head.rc == 0) {
@call(.never_inline, cy.heap.freeObject, .{vm, obj, true, false, true});
if (cy.Trace) {
if (vm.countFrees) {
vm.numFreed += 1;
}
}
@call(.never_inline, cy.heap.freeObject, .{vm, obj, false});
}
}

Expand All @@ -127,9 +122,7 @@ pub inline fn retainObject(self: *cy.VM, obj: *cy.HeapObject) void {
obj.head.rc += 1;
if (cy.Trace) {
checkRetainDanglingPointer(self, obj);
if (cy.TraceRC) {
log.tracevIf(log_mem, "{} +1 retain: {s}", .{obj.head.rc, self.getTypeName(obj.getTypeId())});
}
log.tracevIf(log_mem, "{} +1 retain: {s}", .{obj.head.rc, self.getTypeName(obj.getTypeId())});
}
if (cy.TrackGlobalRC) {
self.c.refCounts += 1;
Expand Down Expand Up @@ -172,9 +165,7 @@ pub inline fn retain(self: *cy.VM, val: cy.Value) void {
const obj = val.asHeapObject();
if (cy.Trace) {
checkRetainDanglingPointer(self, obj);
if (cy.TraceRC) {
log.tracev("{} +1 retain: {s}", .{obj.head.rc, self.getTypeName(obj.getTypeId())});
}
log.tracevIf(log_mem, "{} +1 retain: {s}", .{obj.head.rc, self.getTypeName(obj.getTypeId())});
}
obj.head.rc += 1;
if (cy.TrackGlobalRC) {
Expand All @@ -194,9 +185,7 @@ pub inline fn retainInc(self: *cy.VM, val: cy.Value, inc: u32) void {
const obj = val.asHeapObject();
if (cy.Trace) {
checkRetainDanglingPointer(self, obj);
if (cy.TraceRC) {
log.tracev("{} +{} retain: {s}", .{obj.head.rc, inc, self.getTypeName(obj.getTypeId())});
}
log.tracevIf(log_mem, "{} +{} retain: {s}", .{obj.head.rc, inc, self.getTypeName(obj.getTypeId())});
}
obj.head.rc += inc;
if (cy.TrackGlobalRC) {
Expand Down Expand Up @@ -252,27 +241,26 @@ fn performMark(vm: *cy.VM) !void {
fn performSweep(vm: *cy.VM) !c.GCResult {
log.tracev("Perform sweep.", .{});
// Collect cyc nodes and release their children (child cyc nodes are skipped).
if (cy.Trace) {
vm.countFrees = true;
vm.numFreed = 0;
}
defer {
if (cy.Trace) {
vm.countFrees = false;
}
}
var cycObjs: std.ArrayListUnmanaged(*cy.HeapObject) = .{};
defer cycObjs.deinit(vm.alloc);
// TODO: Report `num_freed` after flattening recursive release.
const num_freed: u32 = 0;
var num_cyc_freed: u32 = 0;

log.tracev("Sweep heap pages.", .{});
for (vm.heapPages.items()) |page| {
var i: u32 = 1;
while (i < page.objects.len) {
const obj = &page.objects[i];
if (obj.freeSpan.typeId != cy.NullId) {
if (obj.isGcConfirmedCyc()) {
try cycObjs.append(vm.alloc, obj);
cy.heap.freeObject(vm, obj, true, true, false);
if (obj.isNoMarkCyc()) {
log.tracev("gc free: {s}, rc={}", .{vm.getTypeName(obj.getTypeId()), obj.head.rc});
if (cy.Trace) {
checkDoubleFree(vm, obj);
}
if (cy.TrackGlobalRC) {
vm.c.refCounts -= obj.head.rc;
}
cy.heap.freeObject(vm, obj, true);
num_cyc_freed += 1;
} else if (obj.isGcMarked()) {
obj.resetGcMarked();
}
Expand All @@ -288,37 +276,32 @@ fn performSweep(vm: *cy.VM) !c.GCResult {
log.tracev("Sweep non-pool cyc nodes.", .{});
var mbNode: ?*cy.heap.DListNode = vm.cyclableHead;
while (mbNode) |node| {
// Obtain next before node is freed.
mbNode = node.next;

const obj = node.getHeapObject();
if (obj.isGcConfirmedCyc()) {
try cycObjs.append(vm.alloc, obj);
cy.heap.freeObject(vm, obj, true, true, false);
if (obj.isNoMarkCyc()) {
log.tracev("gc free: {s}, rc={}", .{vm.getTypeName(obj.getTypeId()), obj.head.rc});
if (cy.Trace) {
checkDoubleFree(vm, obj);
}
if (cy.TrackGlobalRC) {
vm.c.refCounts -= obj.head.rc;
}
cy.heap.freeObject(vm, obj, true);
num_cyc_freed += 1;
} else if (obj.isGcMarked()) {
obj.resetGcMarked();
}
mbNode = node.next;
}

// Free cyc nodes.
for (cycObjs.items) |obj| {
log.tracev("cyc free: {s}, rc={}", .{vm.getTypeName(obj.getTypeId()), obj.head.rc});
if (cy.Trace) {
checkDoubleFree(vm, obj);
}
if (cy.TrackGlobalRC) {
vm.c.refCounts -= obj.head.rc;
}
// No need to bother with their refcounts.
cy.heap.freeObject(vm, obj, false, false, true);
}

if (cy.Trace) {
vm.c.trace.numCycFrees += @intCast(cycObjs.items.len);
vm.numFreed += @intCast(cycObjs.items.len);
vm.c.trace.numCycFrees += num_cyc_freed;
}

const res = c.GCResult{
.numCycFreed = @intCast(cycObjs.items.len),
.numObjFreed = if (cy.Trace) vm.numFreed else 0,
.numCycFreed = num_cyc_freed,
.numObjFreed = num_freed,
};
log.tracev("gc result: num cyc {}, num obj {}", .{res.numCycFreed, res.numObjFreed});
return res;
Expand Down Expand Up @@ -396,7 +379,7 @@ fn markValue(vm: *cy.VM, v: cy.Value) void {
}
},
bt.MapIter => {
markValue(vm, cy.Value.initNoCycPtr(obj.mapIter.map));
markValue(vm, obj.mapIter.map);
},
bt.Closure => {
const vals = obj.closure.getCapturedValuesPtr()[0..obj.closure.numCaptured];
Expand Down
4 changes: 2 additions & 2 deletions src/builtins/bindings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,12 @@ pub fn listResize(vm: *cy.VM, args: [*]const Value, _: u8) Value {
pub fn mapIterator(vm: *cy.VM, args: [*]const Value, _: u8) Value {
const obj = args[0].asHeapObject();
vm.retainObject(obj);
return vm.allocMapIterator(&obj.map) catch fatal();
return vm.allocMapIterator(args[0]) catch fatal();
}

pub fn mapIteratorNext(vm: *cy.VM, args: [*]const Value, _: u8) Value {
const obj = args[0].asHeapObject();
const map: *cy.ValueMap = @ptrCast(&obj.mapIter.map.inner);
const map: *cy.ValueMap = @ptrCast(&obj.mapIter.map.castHeapObject(*cy.heap.Map).inner);
if (map.next(&obj.mapIter.nextIdx)) |entry| {
vm.retain(entry.key);
vm.retain(entry.value);
Expand Down
Loading

0 comments on commit fcd6d95

Please sign in to comment.