diff --git a/benchmark/ffi/get-function.js b/benchmark/ffi/get-function.js new file mode 100644 index 000000000000..3c1e2e974ce6 --- /dev/null +++ b/benchmark/ffi/get-function.js @@ -0,0 +1,48 @@ +'use strict'; + +// Measures symbol resolution rather than call throughput. Creating a callable +// for a fast-eligible signature emits a native trampoline, so this benchmark +// covers the trampoline allocation path that the call benchmarks never reach. +// +// The `fast` variant is eligible for a generated trampoline; `slow` exceeds the +// x86_64 register budget and falls back, so it resolves without allocating one. +// Comparing the two isolates trampoline creation cost from the rest of symbol +// resolution. + +const common = require('../common.js'); +const { DynamicLibrary } = require('node:ffi'); +const { libraryPath, ensureFixtureLibrary } = require('./common.js'); + +const bench = common.createBenchmark(main, { + signature: ['fast', 'slow'], + n: [1e3], +}, { + flags: ['--experimental-ffi'], +}); + +ensureFixtureLibrary(); + +const signatures = { + fast: { name: 'add_i32', return: 'i32', arguments: ['i32', 'i32'] }, + slow: { + name: 'sum_8_i32', + return: 'i32', + arguments: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], + }, +}; + +function main({ n, signature }) { + const { name, ...definition } = signatures[signature]; + const lib = new DynamicLibrary(libraryPath); + + // Warm up one-time initialization (libffi setup, executable memory probe) so + // it is not attributed to the measured resolutions. + lib.getFunction(name, definition); + + bench.start(); + for (let i = 0; i < n; ++i) + lib.getFunction(name, definition); + bench.end(n); + + lib.close(); +} diff --git a/src/ffi/platforms/x64.cc b/src/ffi/platforms/x64.cc index e105eabc13db..d5c15c381574 100644 --- a/src/ffi/platforms/x64.cc +++ b/src/ffi/platforms/x64.cc @@ -294,7 +294,16 @@ void* AllocateCodeNear(uintptr_t target_address, size_t code_size) { const uintptr_t base = target_address & ~(page_size - 1); // Search a small window around the target first. Shared libraries usually // leave nearby holes, and keeping the trampoline close enables jmp rel32. - constexpr uintptr_t kMaxPages = 1024; + // + // The window doubles as the capacity of the near-text region: every + // trampoline keeps one page in it, so about 2 * kMaxPages trampolines per + // library can use jmp rel32 before the window is full and later ones take + // the far placement below. Each candidate costs an mmap syscall that is + // expected to fail, and a layout with no hole within a few pages of the + // text rarely has one further out either, so a wide window mostly buys + // failed probes. Keep it small enough that an exhausted window costs a few + // microseconds while still covering typical per-library symbol counts. + constexpr uintptr_t kMaxPages = 16; for (uintptr_t i = 1; i <= kMaxPages; i++) { const uintptr_t delta = i * page_size; const uintptr_t candidates[] = {