惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
人人都是产品经理
人人都是产品经理
M
MIT News - Artificial intelligence
C
Cybersecurity and Infrastructure Security Agency CISA
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
爱范儿
爱范儿
Spread Privacy
Spread Privacy
P
Palo Alto Networks Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
The Blog of Author Tim Ferriss
The Cloudflare Blog
WordPress大学
WordPress大学
小众软件
小众软件
H
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Blog — PlanetScale
Blog — PlanetScale
V2EX - 技术
V2EX - 技术
H
Hacker News: Front Page
Last Week in AI
Last Week in AI
D
DataBreaches.Net
V
V2EX
S
Securelist
C
CXSECURITY Database RSS Feed - CXSecurity.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
Cloudbric
Cloudbric
月光博客
月光博客
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
N
News and Events Feed by Topic
K
Kaspersky official blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
Secure Thoughts
S
Security Affairs
W
WeLiveSecurity
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
AI
AI
有赞技术团队
有赞技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
罗磊的独立博客
Google DeepMind News
Google DeepMind News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
N
Netflix TechBlog - Medium
PCI Perspectives
PCI Perspectives
SecWiki News
SecWiki News
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog

Hacker News

Introducing Claude Opus 4.7 Qwen Studio The Future of Everything is Lies, I Guess: Where Do We Go From Here? GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis Moving a large-scale metrics pipeline from StatsD to OpenTelemetry / Prometheus GitHub - Nightmare-Eclipse/RedSun: The Red Sun vulnerability repository GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - macOS26/Agent: Any AI, replaces Claude Code, Cursor, OpenClaw. Over 18 LLM providers (Claude, OpenAI, Gemini, Ollama, Zai, HF, Qwen) wired into a native Mac app that writes code, builds Xcode projects, bumps versions, manages git, automates Safari, use AppleScript, JS or Accessibility, extend Agent! w/ MCP Servers, run tasks from your iPhone via Messages. YouTube now lets you turn off Shorts I Made a Terminal Pager Burgers | マクドナルド公式 Commands — HackerNews CLI documentation ChatGPT for Excel PiCore - Raspberry Pi Port of Tiny Core Linux Live Nation illegally monopolized ticketing market, jury finds Google Broke Its Promise to Me. Now ICE Has My Data. Founding Engineer at Adaptional | Y Combinator CRISPR takes important step toward silencing Down syndrome’s extra chromosome GitHub - saffron-health/libretto: The AI toolkit for building reliable browser automations US v. Heppner (S.D.N.Y. 2026) no attorney-client privilege for AI chats [pdf] Unexpected €54k billing spike in 13 hours: Firebase browser key without API restrictions used for Gemini requests Retrofitting JIT Compilers into C Interpreters IPv6 – Google The Accursèd Alphabetical Clock Cybersecurity Looks Like Proof of Work Now Fragments: April 14 Cal.com Goes Closed Source: Why AI Security Is Forcing Our Decision | Cal.com - Scheduling Software for Online Bookings Laravel raised money and now injects ads directly into your agent When moving fast, talking is the first thing to break Too much Discussion of the XOR swap trick – Heather Cafe Introduction to Spherical Harmonics for Graphics Programmers The Grand Line Building a Z-Machine in the worst possible language High-Level Rust: Getting 80% of the Benefits with 20% of the Pain GitHub - duguyue100/midnight-captain: Inspired by Midnight Commander, tailored to my taste. How to build a `git diff` driver · Jamie Tanna | Software Engineer Center for Responsible, Decentralized Intelligence at Berkeley The Local Universe’s Expansion Rate Is Clearer Than Ever, but Still Doesn’t Add Up - A new synthesis of astronomical measurements confirms a persistent mismatch that could point to physics beyond current models The air throughout our homes is infused with microplastics. But there are things you can do to breathe less of them The disturbing white paper Red Hat is trying to erase from the internet – OSnews The Future of Everything is Lies, I Guess: Annoyances ‘Abhorrent’: the inside story of the Polymarket gamblers betting millions on war Productive procrastination — Max van IJsselmuiden maps, territory and LMs 447 Terabytes per Square Centimetre at Zero Retention Energy: Non-Volatile Memory at the Atomic Scale on Fluorographane Show HN: Pardonned.com – A searchable database of US Pardons 20 Years on AWS and Never Not My Job The Seasons are Wrong Artemis II crew splashes down near San Diego after historic moon mission We gave an AI a 3 year retail lease in SF and asked it to make a profit | Andon Labs How a dancer with ALS used brainwaves to perform live On filing the corners off my MacBooks Installing every* Firefox extension OpenClaw’s memory is unreliable, and you don’t know when it will break Steve Blank Nowhere Is Safe Chimpanzees in Uganda locked in vicious 'civil war', say researchers watgo - a WebAssembly Toolkit for Go linux/Documentation/process/coding-assistants.rst at master · torvalds/linux GitHub - callumlocke/json-formatter: Makes JSON easy to read. Founding Product Engineer at Bild AI | Y Combinator A compelling title that is cryptic enough to get you to take action on it GitHub - Keychron/Keychron-Keyboards-Hardware-Design: Industrial design files for Keychron keyboards and mice. 100+ models with CAD assets in STEP, DXF, DWG, and PDF. Source-available, with commercial use allowed for original compatible accessories within the license terms. [ANNOUNCE] WireGuardNT v0.11 and WireGuard for Windows v0.6 Released 1D-Chess Helium Is Hard to Replace Cooperative Vectors Introduction | Evolve Keeping a Postgres queue healthy — PlanetScale Our response to the Axios developer tool compromise Do Americans read print books, e-books or audiobooks more? The Zettelkasten Method in Obsidian: A Practical Setup Guide Artemis II Is Competency Porn and We Are Starving For It WeakC4 Flight Viz — Cockpit View A Mexican surveillance giant you’ve never heard of is now watching the U.S. border Surelock: Deadlock-Free Mutexes for Rust RISC-V 101 – what is it and what does it mean for Canonical? | Ubuntu The Problem That Built an Industry How Much Linear Memory Access Is Enough? | Solidean Investigating Split Locks on x86-64 Simplest hash functions Sybilproof reputation mechanisms (2005) [pdf] What is a property? How Complex is my Code? Static code analysis in Kotlin — tools overview Toffoli gates are all you need PGLite evangelism dcmake: a new CMake debugger UI Clojure on Fennel part one: Persistent Data Structures Fragments: April 2 Python Release Python install manager 26.1 The Life and Death of the Book Review - Liberties Introducing Database Traffic Control — PlanetScale Bitcoin miners are losing $19,000 on every BTC produced as difficulty drops 7.8% God sleeps in the minerals Building slogbox Apple Silicon and Virtual Machines: Beating the 2 VM Limit Who was “Not Even Wrong” first? Pokemon Evolution Vs Darwinian Evolution The APL Programming Language Source Code
Performance improvements in libffi
Anthony Green · 2026-06-21 · via Hacker News

libffi is a function call interpreter. You hand it a description of a function’s signature at runtime, and it works out, on the spot, how to place each argument and make the call. It interprets the calling convention the way a bytecode VM interprets instructions. Nothing is compiled ahead of time, because the whole point is that you don’t know the signature ahead of time.

An interpreter is not what you reach for when you want speed. The usual answer is to JIT: compile a bespoke call stub for each signature, native code that drops the arguments into their registers and jumps, with nothing left to interpret at runtime. It’s quicker, but it gets there by writing fresh machine code into memory that’s both writable and executable, which is exactly what modern systems are trying to stamp out.

So libffi stays an interpreter, on purpose. The question I set out to answer was how much faster it could get that way, by reusing what it already knows instead of generating code at runtime or mapping any page writable and executable.

The waste

When you call a function through libffi, the work splits across two places. ffi_prep_cif runs once per signature. It classifies the whole thing, but it keeps only two results: the size of the stack frame the call will need, and a small code for how the return value comes back. The frame size has to be known before the call is built, because any argument that doesn’t fit in a register spills to the stack, and that space is reserved up front. The return code is for afterward, because the result comes back in rax, or xmm0, or memory depending on the type, and something has to know where to read it from. Both are small and fixed-size, so they live in the ffi_cif. What prep throws away is the part it spent most of its time on: where each individual argument goes.

So on every ffi_call, the marshalling code walks the argument list again and re-derives that placement from scratch before copying the values into place. For a three-argument call on x86-64 that’s around 650 instructions of bookkeeping, and it produces the identical answer every single time.

Most of those instructions aren’t moving argument bytes. They’re deciding where the bytes go. The System V AMD64 ABI classifies every argument by a fixed procedure, and running that procedure on a single argument means walking its type, recursing into a struct’s fields and chasing the pointers in its type descriptor, sorting each 8-byte chunk into an INTEGER or SSE register class, and checking whether it still fits in the registers that are left or has to spill to the stack. That is branch-heavy, pointer-chasing work, the sort a CPU runs slowly, and it reruns on every call to compute a placement that never changes.

But function argument placement is a pure function of the signature. We can compute it once, remember it, and skip the work on every later call.

A plan

The fix is a “plan”: the placement compiled into a flat list of moves, a tiny bytecode for one signature. If ffi_call re-deriving the placement on every call is like interpreting a program by re-walking its syntax tree each time, the plan is the compiled bytecode: the tree-walk happens once, and every later call just runs the flat list. build_plan walks the argument types once, classifies each one the way the ABI rules say, and emits a move per piece: this 8-byte word goes in rdi, that 32-bit int gets sign-extended into rsi, this double lands in an SSE slot, that oversized thing spills to the stack. With the plan in hand, making the call is just running the moves. No re-classification.

Building a call plan, then running it

The opcodes are deliberately dumb. GP64 copies a word into a general register; SE8/SE16/SE32 sign-extend a narrow int; SSE64/SSE32 move a float; STACK memcpys a spilled argument. A three-argument call compiles to three or four of them. Here’s what two real signatures turn into:

long (void *, void *, void *)    long (void *, int, void *)
  GP64  avalue[0] -> rdi           GP64  avalue[0] -> rdi
  GP64  avalue[1] -> rsi           SE32  avalue[1] -> rsi   (sign-extend)
  GP64  avalue[2] -> rdx           GP64  avalue[2] -> rdx
  => all GP64: thunk               => has an SE32: interpret

When every argument is a single 64-bit value in a general register, which is most pointer-passing code, the plan doesn’t even need the interpreter. It’s marked thunk-eligible, and a small hand-written thunk in .text loads the values straight from the argument array into the argument registers and calls. It skips the move loop, the intermediate register image, and the copying back and forth entirely. The call on the right keeps an int, so it needs the sign-extend, so it runs the move loop instead.

There’s a subtlety in running the moves. The loop never loads an actual argument register, because C gives you no way to drop a value into rdi and hold it there across a call; the compiler owns the registers. So each move writes into a plain memory struct that mirrors the System V register file, the six integer registers and eight SSE registers laid out in order, and only once that image is built does a short assembly trampoline load every argument register from it in one shot and jump to the target. The C code moves bytes around in memory; the registers get their final values all at once, in .text, immediately before the call. That trampoline is the same one ffi_call has always used, so the plan changes when the placement is computed, not how the registers get loaded.

The plan is plain data, and the thunk ships in the binary’s read-only text like any other function. Nothing is ever both writable and executable, the same property closures already get from static trampolines.

Build it once, invoke it many times

The plan is exposed as a small, opt-in API. You build a plan from a prepared ffi_cif, invoke it as many times as you like, and free it when you’re done:

ffi_call_plan *plan = ffi_call_plan_alloc(&cif);   /* build the plan once */

ffi_call_plan_invoke(plan, fn, &rv, av);           /* invoke it, no per-call setup */
/* ... invoke it again, and again ... */

ffi_call_plan_free(plan);

ffi_call itself is untouched. A binding that already caches an ffi_cif per signature, which is most of them, caches a plan beside it and calls through ffi_call_plan_invoke. The plan is immutable once built, so one plan can be shared and invoked from any thread without a lock. A signature the fast path can’t handle is still fine: invoke falls back to ffi_call for it.

The numbers

This is the fair comparison: one libffi, the same function, reached three ways. A plain direct call to it, the same call through ffi_call, and the same call through a prebuilt plan. Same binary, same machine (a Core Ultra 7 255H), same -O2, so the only thing that differs between the two FFI rows is the API. The timed loop is just this, over and over:

ffi_type *at[] = { &ffi_type_pointer, &ffi_type_pointer, &ffi_type_pointer };
ffi_cif cif;
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 3, &ffi_type_sint64, at);

ffi_call_plan *plan = ffi_call_plan_alloc(&cif);          /* built once */

void *av[] = { &a, &b, &c };
long rv;
ffi_call_plan_invoke(plan, (void(*)(void))fn, &rv, av);   /* <-- this is what we time */
ptr(p,p,p)                ns/call   vs a regular call
regular function call        1.9       1x
ffi_call_plan_invoke         5.1       2.7x
ffi_call                    31.0       16x

Calling that function the normal way through ffi_call costs about 16 times what a direct call to it costs. Through a prebuilt plan it’s under 3 times. The plan is about 6x faster than ffi_call, and since it’s the same library reached two ways, that gap is the API and nothing else.

Most of what the plan removes is the per-call re-classification: ffi_call rebuilds the placement every time, while invoke just runs the prebuilt moves. On this shape the plan takes the thunk, so it skips the register image too and lands close to a plain call: about 3 ns of FFI overhead on top of a 2 ns call, against 29 ns for ffi_call.

Mixed integer and floating-point signatures don’t take the thunk, because a 32-bit int needs sign extension and a double needs an SSE register, so they run the move loop and land a little higher. They still skip the re-classification. A struct-by-value argument has no plan, so invoke falls back to ffi_call and costs exactly what it did before.

Where the calls actually go

A 6x number on one shape only matters if real programs use that shape, and call it often enough that building a plan once pays off. So I traced one.

GNOME Shell is a good stress test: the entire desktop UI is JavaScript calling into C through GObject Introspection, which calls through libffi. I attached an eBPF uprobe to ffi_call with Whistler and watched for a while. The top signatures looked like this:

21744  int   (void *)
19139  void *(void *, unsigned long)
13083  void *(void *)
10116  void  (void *, void *, void *, long, void *)
 9918  void *(void *, void *)

Around 90% of the calls are pure 64-bit-GP, pointers and longs, which is the thunk path. Not a single by-value struct argument showed up in over a hundred thousand calls. And these are the same handful of signatures called over and over, exactly the shape that rewards building a plan once and invoking it forever. A binding like GObject Introspection already holds an ffi_cif per signature; a plan slots in right beside it.

This all lives on the HEAD of the libffi git tree, not in any release, and it needs more testing before it’s something to build on. The acceleration is x86-64 only, but the API is portable: everywhere else ffi_call_plan_invoke just calls ffi_call, so a binding can build a plan for every signature unconditionally and take the accelerated path where it exists, no #ifdef on its side. Whether the fast path is worth building for other ABIs isn’t clear: the payoff is proportional to how much per-call classification there is to skip, and that varies a lot between calling conventions.

The code is on GitHub: libffi.

Discuss on Hacker News.


Edited for clarity after publishing: 6fed8af.