Repository navigation
ffi: AllocateCodeNear() probes up to 2048 pages with a failing mmap each, costing ~790 µs per trampoline #64968
Copy link
Copy link
Closed
Labels
ffiIssues and PRs related to experimental Foreign Function Interface support.Issues and PRs related to experimental Foreign Function Interface support.
Description
Version
main
Platform
Linux
Subsystem
ffi
What steps will reproduce the bug?
repro.c
#include <stdint.h>
// Fast-eligible: 2 register arguments, scalar return. Resolving this signature
// makes CreateFastFFIMetadata() emit a trampoline through AllocateCodeNear().
int32_t add2(int32_t a, int32_t b) {
return a + b;
}
// Fast-ineligible baseline: 9 arguments exceeds V8's fast-call cap, so no
// trampoline is emitted and AllocateCodeNear() is never reached.
int32_t add9(int32_t a, int32_t b, int32_t c, int32_t d, int32_t e, int32_t f,
int32_t g, int32_t h, int32_t i) {
return a + b + c + d + e + f + g + h + i;
}repro.js
import { DynamicLibrary, suffix } from 'node:ffi';
import { stat } from 'node:fs/promises';
const libPath = new URL(`./repro.${suffix}`, import.meta.url).pathname;
await stat(libPath);
const fastEligible = { arguments: ['i32', 'i32'], return: 'i32' };
const fastIneligible = { arguments: Array(9).fill('i32'), return: 'i32' };
const lib = new DynamicLibrary(libPath);
function timeResolve(name, signature) {
const start = performance.now();
lib.getFunction(name, signature);
return (performance.now() - start) * 1000;
}
// Warm up one-time initialization (libffi setup, executable-memory self-test)
// so it is not attributed to the first measured resolution.
lib.getFunction('add9', fastIneligible);
lib.getFunction('add2', fastEligible);
// Same symbol resolution work, but no trampoline is allocated.
console.log(`add9, no trampoline: ${timeResolve('add9', fastIneligible).toFixed(1)} us`);
console.log(`add2, first probe: ${timeResolve('add2', fastEligible).toFixed(1)} us`);
// Every fast-eligible resolution allocates another trampoline page next to the
// library, so the free pages inside AllocateCodeNear()'s +/- 1024 page probe
// window are steadily consumed. Once they are gone, each allocation walks the
// whole window, fails every MAP_FIXED_NOREPLACE probe, and takes the plain-mmap
// fallback it could have taken immediately.
const samples = [];
for (let i = 0; i < 1200; i++) samples.push(timeResolve('add2', fastEligible));
for (let i = 0; i < samples.length; i += 200) {
const window = samples.slice(i, i + 200);
const mean = window.reduce((a, b) => a + b, 0) / window.length;
console.log(`add2, resolves ${i + 1}-${i + window.length}: mean ${mean.toFixed(1)} us`);
}
lib.close();Commands to run:
$ cc -shared -fPIC -O2 -o repro.so repro.c
$ node --experimental-ffi repro.jsHow often does it reproduce? Is there a required condition?
Always
What is the expected behavior? Why is that the expected behavior?
Resolving a fast-eligible signature costs about as much as a fast-ineligible one, since the extra work is a single page allocation.
What do you see instead?
add9, no trampoline: 29.0 us
add2, first probe: 31.1 us
add2, resolves 1-200: mean 517.5 us
add2, resolves 201-400: mean 657.0 us
add2, resolves 401-600: mean 728.0 us
add2, resolves 601-800: mean 751.0 us
add2, resolves 801-1000: mean 753.1 us
add2, resolves 1001-1200: mean 755.4 usOnce the pages near the library are used up, each fast-eligible resolution costs ~28x a fast-ineligible one, spent entirely on ~2048 mmap calls that are all expected to fail.
Additional information
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
ffiIssues and PRs related to experimental Foreign Function Interface support.Issues and PRs related to experimental Foreign Function Interface support.
Activity
trivikr commentedon Aug 2, 2026
Proposed setting
kMaxPages=16in #64969ffi: shrink trampoline placement probe window
ffi: shrink trampoline placement probe window
ffi: shrink trampoline placement probe window