Skip to content

A more cursed technique... =) #3

@billywhizz

Description

@billywhizz

Thanks for this Bryan - i didn't know this was possible. Out of interest, i ran some benchmarks of this on a custom v8 runtime i am hacking on and compared it to another technique i have been playing with. Of course, this is very dangerous and not something I would expect to see in Node.js or Deno, but the numbers are interesting all the same.

The technique I use is:

  • use system calloc (using FFI or bindings) to allocate the memory and get back an address
  • wrap the allocated memory in a backing store with an empty deleter so it won't ever get freed by v8
  • use system free (using FFI or bindings) to free the memory when we are done. the wrapping ArrayBuffer should be collected at some point by GC

This proves to be ~30 times faster on my setup, but your detach technique does not seem to work for me in freeing up the memory for the wrapping ArrayBuffer in the hot loop so I see memory constantly growing.

Screenshot from 2023-09-18 15-20-36

this is what the JS code looks like. I had to set the --allow-natives-syntax flag on the command line as v8 i am on barfs when i try to change the flags after initialising v8 platform.

import { Bench } from 'lib/bench.js'
import { system } from 'lib/system.js'

const { wrapMemory } = spin

const handle = {
  buffer: new ArrayBuffer(0),
  address: 0
}

function allocCBuffer(size) {
  const address = system.calloc(1, size)
  handle.buffer = wrapMemory(address, address + size)
  handle.address = address
}

function makeDetach () {
  const internalDetach = new Function('buf', '%ArrayBufferDetach(buf)')
  return function detach (buf) {
    if (buf.buffer) {
      if (buf.byteOffset !== 0 || buf.byteLength !== buf.buffer.byteLength) return
      buf = buf.buffer
    }
    internalDetach(buf)
  }
}

const detach = makeDetach()

const bench = new Bench()

while (1) {


for (let i = 0; i < 5; i++) {
  bench.start('buffers')
  for (let j = 0; j < 2500; j++) {
    const buf = new ArrayBuffer(100 * 1024 * 1024)
  }
  bench.end(2500)
}

for (let i = 0; i < 5; i++) {
  bench.start('buffers detach')
  for (let j = 0; j < 3000; j++) {
    const buf = new ArrayBuffer(100 * 1024 * 1024)
    detach(buf)
  }
  bench.end(3000)
}

for (let i = 0; i < 5; i++) {
  bench.start('c-buffers')
  for (let j = 0; j < 100000; j++) {
    allocCBuffer(100 * 1024 * 1024)
    system.free(handle.address)
    detach(handle.buffer)
  }
  bench.end(100000)
}

}

Will have a further look when I get a chance and hopefully I can share this code soon.

v8/C++ WrapMemory Function

void spin::WrapMemory(const FunctionCallbackInfo<Value> &args) {
  Isolate* isolate = args.GetIsolate();
  uint64_t start64 = (uint64_t)Local<Integer>::Cast(args[0])->Value();
  uint64_t end64 = (uint64_t)Local<Integer>::Cast(args[1])->Value();
  const uint64_t size = end64 - start64;
  void* start = reinterpret_cast<void*>(start64);
  int free = 0;
  if (args.Length() > 2) free = Local<Integer>::Cast(args[2])->Value();
  if (free == 0) {
    std::unique_ptr<BackingStore> backing = ArrayBuffer::NewBackingStore(
        start, size, v8::BackingStore::EmptyDeleter, nullptr);
    // this line causes memory allocation that never seems to be collected
    Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, std::move(backing));
    args.GetReturnValue().Set(ab);
    return;
  }
  std::unique_ptr<BackingStore> backing = ArrayBuffer::NewBackingStore(
      start, size, spin::FreeMemory, nullptr);
  Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, std::move(backing));
  args.GetReturnValue().Set(ab);
}

this is all horribly dangerous of course, but it's fun to test the boundaries of what v8/JS can do I think.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions