-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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.
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.
