Skip to content

GC/RT: Deferred Reference Counting? #1534

Closed
@dcodeIO

Description

@dcodeIO

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 from null
  • 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 a pool_create incl. the necessary additional local at the start of a function, and a pool_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 redundant retain/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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions