|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const assert = require('node:assert'); |
| 4 | +const common = require('../common.js'); |
| 5 | +const { libraryPath, ensureFixtureLibrary } = require('./common.js'); |
| 6 | + |
| 7 | +// Measure the invocation (call) path for signatures that bypass V8 Fast API |
| 8 | +// and use libffi through FFIFunction::Invoke(). On x86-64 System V with |
| 9 | +// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating |
| 10 | +// argument-placement work on every call. This benchmark quantifies the |
| 11 | +// per-call benefit. |
| 12 | +// |
| 13 | +// Signatures chosen to bypass both V8 Fast API and keep native work minimal: |
| 14 | +// - call_int_callback (null): 'function' type forces the generic path; null |
| 15 | +// pointer triggers the early return in C so native computation is negligible. |
| 16 | +// From libffi's perspective this is a register-only plan (2 pointer-sized |
| 17 | +// args both fit in GP registers on x86-64 System V). |
| 18 | +// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing |
| 19 | +// the generic path. From libffi's perspective 6 args go in registers and 2 |
| 20 | +// spill to the stack, exercising a stack-spilled plan. |
| 21 | + |
| 22 | +const bench = common.createBenchmark(main, { |
| 23 | + n: [1e7], |
| 24 | + symbol: ['call_int_callback', 'sum_8_i32'], |
| 25 | +}, { |
| 26 | + flags: ['--experimental-ffi', '--no-warnings'], |
| 27 | +}); |
| 28 | + |
| 29 | +ensureFixtureLibrary(); |
| 30 | + |
| 31 | +function main({ n, symbol }) { |
| 32 | + const ffi = require('node:ffi'); |
| 33 | + |
| 34 | + if (symbol === 'call_int_callback') { |
| 35 | + // 'function' type bypasses Fast API (IsFastCallEligible rejects it). |
| 36 | + // Pass 0n (null function pointer) so the native function returns -1 |
| 37 | + // immediately without invoking any callback, keeping per-call overhead |
| 38 | + // dominated by the FFI call machinery itself. |
| 39 | + const { lib, functions } = ffi.dlopen(libraryPath, { |
| 40 | + call_int_callback: { return: 'i32', arguments: ['function', 'i32'] }, |
| 41 | + }); |
| 42 | + |
| 43 | + try { |
| 44 | + // Verify the null-pointer early return. |
| 45 | + assert.strictEqual(functions.call_int_callback(0n, 7), -1); |
| 46 | + |
| 47 | + bench.start(); |
| 48 | + for (let i = 0; i < n; ++i) |
| 49 | + functions.call_int_callback(0n, 21); |
| 50 | + bench.end(n); |
| 51 | + } finally { |
| 52 | + lib.close(); |
| 53 | + } |
| 54 | + } else { |
| 55 | + // 8 integer args exceed the x86-64 SysV GP register cap (6), which makes |
| 56 | + // CreateFastFFIMetadata reject the signature. Calls go through the |
| 57 | + // SharedBuffer or generic invoker into FFIFunction::Invoke(). |
| 58 | + const { lib, functions } = ffi.dlopen(libraryPath, { |
| 59 | + sum_8_i32: { |
| 60 | + return: 'i32', |
| 61 | + arguments: [ |
| 62 | + 'i32', 'i32', 'i32', 'i32', |
| 63 | + 'i32', 'i32', 'i32', 'i32', |
| 64 | + ], |
| 65 | + }, |
| 66 | + }); |
| 67 | + |
| 68 | + const fn = functions.sum_8_i32; |
| 69 | + |
| 70 | + assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36); |
| 71 | + |
| 72 | + bench.start(); |
| 73 | + for (let i = 0; i < n; ++i) |
| 74 | + fn(1, 2, 3, 4, 5, 6, 7, 14); |
| 75 | + bench.end(n); |
| 76 | + |
| 77 | + lib.close(); |
| 78 | + } |
| 79 | +} |
0 commit comments