| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 044402c commit 5cac7c2
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,79 @@ | |||
| 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 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) { | |||
| 192 | 192 | IsBufferTypeName(fn.arg_type_names[0])); | |
| 193 | 193 | } | |
| 194 | 194 | ||
| 195 | + namespace { | ||
| 196 | + | ||
| 197 | + std::shared_ptr<FFIFunction> CloneForFastMetadata( | ||
| 198 | + const std::shared_ptr<FFIFunction>& fn) { | ||
| 199 | + // Fast metadata only needs the native target and signature. In particular, | ||
| 200 | + // its temporary clone must not borrow the original function's cif or plan. | ||
| 201 | + auto clone = std::make_shared<FFIFunction>(); | ||
| 202 | + clone->closed = fn->closed; | ||
| 203 | + clone->ptr = fn->ptr; | ||
| 204 | + clone->args = fn->args; | ||
| 205 | + clone->return_type = fn->return_type; | ||
| 206 | + clone->arg_type_names = fn->arg_type_names; | ||
| 207 | + clone->return_type_name = fn->return_type_name; | ||
| 208 | + return clone; | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + } // namespace | ||
| 212 | + | ||
| 195 | 213 | std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames( | |
| 196 | 214 | const std::shared_ptr<FFIFunction>& fn) { | |
| 197 | 215 | // The primary Fast API entrypoint receives pointer-compatible values as | |
| 198 | 216 | // BigInts after the JS wrapper has converted strings, nullish values, and | |
| 199 | 217 | // memory-backed objects. A secondary entrypoint handles the monomorphic | |
| 200 | 218 | // memory-backed case without extracting the pointer in JS. | |
| 201 | - auto clone = std::make_shared<FFIFunction>(*fn); | ||
| 219 | + auto clone = CloneForFastMetadata(fn); | ||
| 202 | 220 | for (std::string& name : clone->arg_type_names) { | |
| 203 | 221 | if (IsBufferTypeName(name)) { | |
| 204 | 222 | name = "pointer"; | |
@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames( | |||
| 209 | 227 | ||
| 210 | 228 | std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames( | |
| 211 | 229 | const std::shared_ptr<FFIFunction>& fn) { | |
| 212 | - // Reuse the same native target and libffi metadata, but describe the JS | ||
| 230 | + // Reuse the same native target and signature metadata, but describe the JS | ||
| 213 | 231 | // argument as `buffer` so CreateFastFFIMetadata() emits a trampoline that | |
| 214 | 232 | // receives a V8 value and calls node_ffi_fast_buffer_data(). | |
| 215 | - auto clone = std::make_shared<FFIFunction>(*fn); | ||
| 233 | + auto clone = CloneForFastMetadata(fn); | ||
| 216 | 234 | for (std::string& name : clone->arg_type_names) { | |
| 217 | 235 | if (IsPointerTypeName(name)) { | |
| 218 | 236 | name = "buffer"; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -42,6 +42,17 @@ using v8::Value; | |||
| 42 | 42 | ||
| 43 | 43 | namespace ffi { | |
| 44 | 44 | ||
| 45 | + void FFIFunction::Invoke(void* result, void** values) { | ||
| 46 | + #if defined(NODE_FFI_HAS_FAST_CALL_PLAN) | ||
| 47 | + if (call_plan != nullptr) { | ||
| 48 | + ffi_call_plan_invoke(call_plan.get(), FFI_FN(ptr), result, values); | ||
| 49 | + return; | ||
| 50 | + } | ||
| 51 | + #endif | ||
| 52 | + | ||
| 53 | + ffi_call(&cif, FFI_FN(ptr), result, values); | ||
| 54 | + } | ||
| 55 | + | ||
| 45 | 56 | void FFIFunctionInfo::MemoryInfo(MemoryTracker* tracker) const { | |
| 46 | 57 | tracker->TrackField("sb_backing", sb_backing); | |
| 47 | 58 | } | |
@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction( | |||
| 146 | 157 | ||
| 147 | 158 | should_cache_symbol = symbols_.find(name) == symbols_.end(); | |
| 148 | 159 | ||
| 149 | - fn = std::make_shared<FFIFunction>( | ||
| 150 | - FFIFunction{.closed = false, | ||
| 151 | - .ptr = ptr, | ||
| 152 | - .cif = {}, | ||
| 153 | - .args = args, | ||
| 154 | - .return_type = return_type, | ||
| 155 | - .arg_type_names = std::move(arg_type_names), | ||
| 156 | - .return_type_name = std::move(return_type_name)}); | ||
| 160 | + fn = std::make_shared<FFIFunction>(); | ||
| 161 | + fn->ptr = ptr; | ||
| 162 | + fn->args = std::move(args); | ||
| 163 | + fn->return_type = return_type; | ||
| 164 | + fn->arg_type_names = std::move(arg_type_names); | ||
| 165 | + fn->return_type_name = std::move(return_type_name); | ||
| 157 | 166 | ||
| 158 | 167 | ffi_status status = ffi_prep_cif(&fn->cif, | |
| 159 | 168 | FFI_DEFAULT_ABI, | |
@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction( | |||
| 178 | 187 | return {}; | |
| 179 | 188 | } | |
| 180 | 189 | ||
| 190 | + #if defined(NODE_FFI_HAS_FAST_CALL_PLAN) | ||
| 191 | + // Allocation failure is non-fatal. Invoke() falls back to ffi_call(). | ||
| 192 | + ffi_call_plan* call_plan = ffi_call_plan_alloc(&fn->cif); | ||
| 193 | + if (call_plan != nullptr) { | ||
| 194 | + fn->call_plan.reset(call_plan); | ||
| 195 | + } | ||
| 196 | + #endif | ||
| 197 | + | ||
| 181 | 198 | should_cache_function = true; | |
| 182 | 199 | } else { | |
| 183 | 200 | fn = existing->second; | |
@@ -552,7 +569,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) { | |||
| 552 | 569 | result = Malloc(GetFFIReturnValueStorageSize(fn->return_type)); | |
| 553 | 570 | } | |
| 554 | 571 | ||
| 555 | - ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data()); | ||
| 572 | + fn->Invoke(result, ffi_args.data()); | ||
| 556 | 573 | ||
| 557 | 574 | // Return result back to Javascript | |
| 558 | 575 | ToJSReturnValue(env, args, fn->return_type, result); | |
@@ -611,7 +628,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) { | |||
| 611 | 628 | alignas(8) uint8_t result_storage[kSBResultStorageSize] = {0}; | |
| 612 | 629 | void* result = (fn->return_type != &ffi_type_void) ? result_storage : nullptr; | |
| 613 | 630 | ||
| 614 | - ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data()); | ||
| 631 | + fn->Invoke(result, ffi_args.data()); | ||
| 615 | 632 | ||
| 616 | 633 | if (result != nullptr) { | |
| 617 | 634 | WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,20 +14,41 @@ | |||
| 14 | 14 | #include <unordered_map> | |
| 15 | 15 | #include <vector> | |
| 16 | 16 | ||
| 17 | + // libffi only accelerates reusable call plans on x86-64 System V. Other | ||
| 18 | + // targets implement the API by calling ffi_call(), which adds no benefit. | ||
| 19 | + #if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \ | ||
| 20 | + defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \ | ||
| 21 | + !defined(_WIN32) | ||
| 22 | + #define NODE_FFI_HAS_FAST_CALL_PLAN 1 | ||
| 23 | + #endif | ||
| 24 | + | ||
| 17 | 25 | namespace node::ffi { | |
| 18 | 26 | ||
| 19 | 27 | class DynamicLibrary; | |
| 20 | 28 | struct FFIFunction; | |
| 21 | 29 | ||
| 22 | 30 | struct FFIFunction { | |
| 23 | - bool closed; | ||
| 31 | + FFIFunction() = default; | ||
| 32 | + FFIFunction(const FFIFunction&) = delete; | ||
| 33 | + FFIFunction& operator=(const FFIFunction&) = delete; | ||
| 34 | + FFIFunction(FFIFunction&&) = delete; | ||
| 35 | + FFIFunction& operator=(FFIFunction&&) = delete; | ||
| 24 | 36 | ||
| 25 | - void* ptr; | ||
| 26 | - ffi_cif cif; | ||
| 37 | + bool closed = false; | ||
| 38 | + | ||
| 39 | + void* ptr = nullptr; | ||
| 40 | + ffi_cif cif = {}; | ||
| 27 | 41 | std::vector<ffi_type*> args; | |
| 28 | - ffi_type* return_type; | ||
| 42 | + ffi_type* return_type = nullptr; | ||
| 29 | 43 | std::vector<std::string> arg_type_names; | |
| 30 | 44 | std::string return_type_name; | |
| 45 | + #if defined(NODE_FFI_HAS_FAST_CALL_PLAN) | ||
| 46 | + // The plan borrows cif, so it must remain uniquely owned by this instance. | ||
| 47 | + std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{ | ||
| 48 | + nullptr, ffi_call_plan_free}; | ||
| 49 | + #endif | ||
| 50 | + | ||
| 51 | + void Invoke(void* result, void** values); | ||
| 31 | 52 | }; | |
| 32 | 53 | ||
| 33 | 54 | class FFIFunctionInfo final : public BaseObject { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments