Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions benchmark/ffi/get-function.js
Original file line number Diff line number Diff line change
@@ -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();
}
11 changes: 10 additions & 1 deletion src/ffi/platforms/x64.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = {
Expand Down
Loading