Closed
Description
With the changes introduced in #1503, in particular having one more header word to work with, the following deferred mechanism akin to autorelease pools may be possible to improve our ARC implementation:
- Repurpose
gcInfo2
(originally introduced for tracing) to maintain a per-function autorelease pool as a linked list - Let the list's tail be a sentinel value, e.g.
-1
, so it can be distinguished fromnull
- When compiling a function, instead of refcounting locals (to emulate a stack), link objects being kept alive by the function to the function's autorelease pool / list. Typical entries are managed function arguments, foreign function return values and managed values loaded from memory.
- If there's a reachable
pool_track
in a code path, insert apool_create
incl. the necessary additional local at the start of a function, and apool_release
when exiting the function.
// Prototypical implementation
import { OBJECT, TOTAL_OVERHEAD } from "rt/common";
/** Creates a new autorelease pool. */
@inline function pool_create(): usize {
return -1;
}
/** Tracks an object in an autorelease pool. */
function pool_track(pool: usize, ptr: usize): usize {
if (ptr) {
var obj = changetype<OBJECT>(ptr - TOTAL_OVERHEAD);
// Only track objects that are not yet part of a pool. If the object is part
// of a pool already, it is either part of this pool, or of a parent pool,
// which is guaranteed to be released later than this pool.
if (!obj.gcInfo2) {
obj.gcInfo2 = pool;
pool = changetype<usize>(obj);
}
}
return pool;
}
/** Releases an autorelease pool. */
function pool_release(pool: usize): void {
while (pool != -1) {
let obj = changetype<OBJECT>(pool);
pool = obj.gcInfo2;
obj.gcInfo2 = 0;
decrement(changetype<usize>(obj));
}
}
// Example (comments are compiler inserts)
class Obj {}
function foo(a: Obj): Obj { // `a` is likely tracked in an outer pool
/**/var pool = pool_create(); // create pool for the function
/**/pool = pool_track(pool, changetype<usize>(a)); // track `a`, likely already in a pool
var b = bar(a);
/**/pool = pool_track(pool, changetype<usize>(b)); // track `b`, likely not yet in a pool
/**/__retain(b); // retain the return value
/**/pool_release(pool); // release `a` (unlikely), `b` (likely)
return b;
}
function bar(b: Obj): Obj { // `b` is likely tracked in an outer pool
/**/var pool = pool_create(); // create pool for the function
/**/pool = pool_track(pool, changetype<usize>(b)); // track `b`, likely already in a pool
/**/__retain(b); // retain the return value
/**/pool_release(pool); // release `b` (unlikely)
return b;
}
Giving us:
- No need to inject retain/release on locals, greatly simplifying compiler logic in that performing autoreleases is now a runtime instead of a compiler detail. Can then strip functionality like
Constraints.WILL_RETAIN
,LocalFlags.RETAINED
,Compiler#performAutoreleases
,finishAutoreleases
,skippedAutoreleases
,tryUndoAutorelease
,delayAutorelease
etc. from the compiler itself. - Reduces unnecessary write barriers on
release
, in that redundantretain
/release
pairs are implicitly deduplicated by means of adding objects to one pool max. - No additional allocations necessary to implement (i.e. no shadow stack)
I'm likely missing something, but seems promising, so pinning it here. @MaxGraey Thoughts?