Version
main
Platform
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_tadd2(int32_ta, int32_tb) {
returna+b;
}
// Fast-ineligible baseline: 9 arguments exceeds V8's fast-call cap, so no// trampoline is emitted and AllocateCodeNear() is never reached.int32_tadd9(int32_ta, int32_tb, int32_tc, int32_td, int32_te, int32_tf,
int32_tg, int32_th, int32_ti) {
returna+b+c+d+e+f+g+h+i;
}repro.js
import{DynamicLibrary,suffix}from'node:ffi';import{stat}from'node:fs/promises';constlibPath=newURL(`./repro.${suffix}`,import.meta.url).pathname;awaitstat(libPath);constfastEligible={arguments: ['i32','i32'],return: 'i32'};constfastIneligible={arguments: Array(9).fill('i32'),return: 'i32'};constlib=newDynamicLibrary(libPath);functiontimeResolve(name,signature){conststart=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.constsamples=[];for(leti=0;i<1200;i++)samples.push(timeResolve('add2',fastEligible));for(leti=0;i<samples.length;i+=200){constwindow=samples.slice(i,i+200);constmean=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.js
How 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 usadd2, first probe: 31.1 usadd2, resolves 1-200: mean 517.5 usadd2, resolves 201-400: mean 657.0 usadd2, resolves 401-600: mean 728.0 usadd2, resolves 601-800: mean 751.0 usadd2, resolves 801-1000: mean 753.1 usadd2, resolves 1001-1200: mean 755.4 us
Once 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
Version
main
Platform
Subsystem
ffi
What steps will reproduce the bug?
repro.crepro.jsCommands to run:
How 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?
Once the pages near the library are used up, each fast-eligible resolution costs ~28x a fast-ineligible one, spent entirely on ~2048
mmapcalls that are all expected to fail.Additional information
No response