Files
node/benchmark/ffi/invoke-function.js
Matteo Collina 9f04fcd70d ffi: enable module by default
Enable node:ffi by default in builds with FFI support. Keep
--experimental-ffi as a compatibility no-op and --no-experimental-ffi as
an opt-out. Loading the module still emits an experimental warning.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/65475
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
2026-08-28 00:23:11 +00:00

80 lines
2.7 KiB
JavaScript

'use strict';
const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.
const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--no-warnings'],
});
ensureFixtureLibrary();
function main({ n, symbol }) {
const ffi = require('node:ffi');
if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});
try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);
bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});
const fn = functions.sum_8_i32;
assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);
bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);
lib.close();
}
}