From aa9a0751afb1361b8394c9c02be3ba6de947821f Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:34:44 +0000 Subject: [PATCH] ffi: shrink trampoline placement probe window AllocateCodeNear() searched 1024 pages in each direction for a free page next to the native target, one MAP_FIXED_NOREPLACE mmap per candidate. Every probe is expected to fail, so an exhausted window cost up to 2048 failing syscalls before taking the plain-mmap fallback that was already there, all to enable a jmp rel32 instead of movabs+jmp. Resolving a fast-eligible signature plateaued at ~790us, against ~28us for one that allocates no trampoline. Reduce the window to 16 pages per direction, bounding the exhausted case to 32 probes while still covering typical per-library symbol counts. Placement stays opportunistic: the caller already emits the absolute form when EmitJmpRel32() reports the target is out of range. Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Assisted-by: claude:opus-5 --- benchmark/ffi/get-function.js | 48 +++++++++++++++++++++++++++++++++++ src/ffi/platforms/x64.cc | 11 +++++++- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 benchmark/ffi/get-function.js 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[] = {