more posts

Native-only garbage collection

Porffor now has garbage collection (GC)... somewhat. I propose a likely controversial garbage collector implementation: Wasm builds have no GC and native builds have a tightly integrated tiny GC.

No GC for Wasm

I imagine this will be controversial and I will preface that this is just my opinion: GC in Wasm is generally poor (compared to native). Your options for implementing GC in Wasm:

Having a performant and minimal GC simply does not align with Wasm's model, which is fine! There are definitely arguments against this but from what I know and my experience, this is generally true.

I propose that in most situations in which you compile to Wasm, you do not really need GC as they are typically stateless one-shots which can either be re-instantiated or reset between execution. I am sure there are scenarios I am missing, but from what I have seen this is mostly true. You likely would not have long-running server side processes in Wasm; you are either using the Wasm as a lambda-like or using native instead.

A GC for native

Porffor's GC is intentionally as simple as possible. Not only does this generally improve performance but also reduce the attack surface. The GC is ~400 lines of C that replaces Porffor's regular allocator from Wasm in Porffor's own Wasm -> C compiler. Replacing the allocator means we do not need a rewrite, our Wasm code does not know the difference. It is simple: mark-and-sweep with bump allocation (and free-list reuse). For now, it is only used for HTTP servers as it is easier to track roots since we can GC specifically between requests to avoid GC spikes during requests (so this does not work generally with JS yet, but is not far off). There is no compacting or generations as we are talking in hundreds of KBs here with other bottlenecks before GC so it isn't needed yet.

Very simplified pseudo-code for those not familiar with GC:

const roots = // ...
const allObjects = // ...

function mark(root) {
  // Mark root and all its references as safe
  if (root.safe) return;
  root.safe = true;

  const references = // ...
  for (const ref of references) mark(ref);
}

function sweep() {
  // Sweep away all non-safe objects
  // (objects are ~anything JS using memory: JS objects, arrays, strings, ...)
  for (const object of allObjects) {
    // Free if not safe, otherwise mark as unsafe for next collection
    if (!object.safe) {
      free(object);
    } else {
      object.safe = false;
    }
  }
}

function onRequest(requestObject, callback) {
  // Don't sweep request object while request is being processed
  roots.add(requestObject);

  // Run user code to process the request and return the response
  const response = userCode(requestObject);
  callback(response);

  // Remove request object from roots
  roots.remove(requestObject);

  // Mark and sweep
  for (const root of roots) mark(root);
  sweep();
}