25x faster than Ghidra (v1)

Every function of locationd, a macOS daemon, decompiled to C in 12 seconds. Same file, same Mac, fastest of three runs for every tool. Where the 25x comes from, and what the run doesn't show.

Jimmi Andersen19 Oct 2026 · 9 min read · engine 26.10.8

Ghidra 12.1.4 decompiles locationd, a macOS daemon with 25,406 functions, in 303 seconds. openbinary 26.10.8 does it in 12. Same x86_64 slice, same M4 Pro, same day, each tool's fastest of three timed runs. Both find the same functions within 0.5%, so the time isn't bought by skipping any.

locationd, every function to Cwall time, fastest of three runs on one Mac · Ghidra is 1x · Binary Ninja from their published run
openbinary
25x
Binary Ninja (their published run)
7.3x
Ghidra
1x

On a smaller daemon, syspolicyd with 4,174 functions, the gap is 78x. At this speed a decompiler stops being something you open and becomes something you run on everything: every binary in a firmware image, every release, every function, before anyone looks.

Where 25x comes from

Two things multiply. openbinary spends 14x less CPU time than Ghidra on the same file, because it executes 12x fewer instructions, and it keeps 2.4 cores busy against Ghidra's 1.4. Less work, then wider.

  • No JVM — openbinary is one Rust binary; the process starts and is reading the file. Ghidra's time starts with a virtual machine coming up.
  • No project database — Ghidra imports the file into a project, analyses, saves, and the headless run deletes the project. openbinary reads the file, writes one JSON document, exits.
  • Every function in parallel — every architecture lifts to one intermediate language, and every function is lifted and decompiled across cores. CPU time is about twice wall time: 2.4 cores busy on locationd, 2.0 on syspolicyd. Ghidra kept 1.4 and 1.2 busy; radare2 and rizin 1.0, their analysis is single-threaded.
  • A body for every function — Ghidra's run timed out on one syspolicyd function and failed four on locationd. openbinary produced a body for all 29,580. A body isn't the same as a correct body; that gap is in the open items below.

What the speed buys

The lift behind the C feeds everything else openbinary does, so this is the step that sets the price of the rest.

  • Every function gets looked at — findings, taint paths and the attack surface are computed over all 25,406 functions, not the handful an analyst opened.
  • Every binary in the image — a firmware image of hundreds of binaries is minutes, so a fleet, or every release, is a batch job rather than a project.
  • It fits inside a tool call — 12 seconds sits inside a CI check or an agent's tool call. Five minutes doesn't.

Decompilation

Analysis plus a C body for every function. Every figure is from the tool's fastest of three runs after a warm-up; the slowest run is under each table. The machine wasn't idle and load only adds time, so the fastest run is the closest to the true cost. Memory is the peak of the whole process tree, so Ghidra's decompiler children count. Instructions is the CPU instruction count of the run, from the same /usr/bin/time -l call; load moves time but not this column, which varied under 0.1% between runs.

locationd, 25,406 functions

Time (s)CPU (s)Memory (GB)Instructions (billions)FunctionsFailed
openbinary12.0028.91.8242125,4060
Ghidra302.73409.33.834,94125,5314
Binary Ninja*41.4n/a3.622,79028,372n/a

Slowest run: openbinary 12.28 s, Ghidra 427.30 s.

syspolicyd, 4,174 functions

Time (s)CPU (s)Memory (GB)Instructions (billions)FunctionsFailed
openbinary1.733.390.27484,1740
Ghidra134.97157.31.837834,2271
Binary Ninja*7.0n/a2.304104,215n/a

Slowest run: openbinary 1.81 s, Ghidra 156.96 s. Ghidra ran with a 60 s per-function decompile timeout.

* Binary Ninja 6.0 is their published run on their own hardware, not our measurement. radare2 and rizin don't decompile; they're in the next table.

Disassembly

Loading, symbols, strings, function discovery, cross-references and library signatures. No C. radare2 6.2.5 and rizin 0.10.0 ran aaa.

locationd

Time (s)CPU (s)Memory (GB)Instructions (billions)Functions
openbinary4.308.250.2311325,406
Ghidra126.63226.73.462,43825,531
radare279.1576.20.591,02625,442
rizin44.9344.90.7691025,424

Slowest run: radare2 109.21 s under a 1-minute load of 55; the others within 3% of their fastest.

syspolicyd

Time (s)CPU (s)Memory (GB)Instructions (billions)Functions
openbinary0.751.300.13174,174
Ghidra43.7870.41.556514,227
radare27.187.160.091014,180
rizin3.183.140.14604,174

radare2 uses the least memory here, 0.09 GB against openbinary's 0.13 GB.

Ghidra's default analysis already uses its decompiler for switch recovery, so its disassembly run isn't decompiler-free and the two aren't perfectly separable. For openbinary, the C is about 65% of decompilation time on locationd (12.00 s against 4.30 s) and most of the memory (1.82 GB against 0.23 GB). aaaa changed little: radare2 74.91 s and rizin 45.37 s on locationd, 7.38 s and 3.17 s on syspolicyd, with the function counts within one of aaa. The four local tools agree on function count within 0.5% on locationd and 1.3% on syspolicyd.

Side by side

Selection rule: walking Ghidra's decompile of the syspolicyd x86_64 slice in address order, take the first function whose name is not auto-generated (no FUN_, thunk_, _INIT_, entry or __ prefix), whose Ghidra C block ({ to }) is 14 to 40 lines, and which openbinary also decompiled at the same address. That is -[SPSamplingTasking encodeWithCoder:] at 0x1000025cf.

openbinary 26.10.8
struct self_s { uint64_t field_8; uint64_t field_10; uint64_t field_18; uint64_t field_20; uint64_t field_28; }; void -[SPSamplingTasking encodeWithCoder:](struct self_s * self, id a1){    uint64_t r12_1;    uint64_t r13_1;    uint64_t r14_1;    uint64_t rax_7;     r12_1 = self->uuid;    r14_1 = *(uint64_t *)0x1001204e8;    (*(uint64_t *)0x1000fef68)(a1);    r13_1 = *(uint64_t *)0x1000fef20;    (r13_1)(a1, r14_1, r12_1, 0x1001037c8);    (r13_1)(a1, r14_1, self->name, 0x1001037e8);    (r13_1)(a1, r14_1, self->expiration, 0x100103808);    (r13_1)(a1, r14_1, self->samplingRate, 0x100103828);    (r13_1)(a1, r14_1, self->publicKey, 0x100103848);    rax_7 = (*(uint64_t *)0x1000fef60)(a1);    return;}
Ghidra 12.1.4
/* Function Stack Size: 0x18 bytes */ void SPSamplingTasking::encodeWithCoder_(ID param_1,SEL param_2,ID param_3) {  undefined8 uVar1;  undefined *puVar2;  undefined *puVar3;    puVar3 = PTR_s_encodeObject_forKey__1001204e8;  uVar1 = *(undefined8 *)(param_1 + 8);  (*(code *)PTR__objc_retain_1000fef68)(param_3);  puVar2 = PTR__objc_msgSend_1000fef20;  (*(code *)PTR__objc_msgSend_1000fef20)(param_3,puVar3,uVar1,&cf__uuid);  (*(code *)puVar2)(param_3,puVar3,*(undefined8 *)(param_1 + 0x10),&cf__name);  (*(code *)puVar2)(param_3,puVar3,*(undefined8 *)(param_1 + 0x18),&cf__expiration);  (*(code *)puVar2)(param_3,puVar3,*(undefined8 *)(param_1 + 0x20),&cf__samplingRate);  (*(code *)puVar2)(param_3,puVar3,*(undefined8 *)(param_1 + 0x28),&cf__publicKey);  (*(code *)PTR__objc_release_1000fef60)(param_3);  return;}

Both are straight-line code with the same five calls through objc_msgSend. Each tool wins half of it.

  • openbinary keeps the method — the Objective-C signature survives, and the ivars are read by name: self->uuid, self->name, self->expiration, self->samplingRate, self->publicKey. Ghidra shows raw offsets (param_1 + 0x10) and C++-style ID and SEL parameters. One defect of ours is visible: the struct declaration above the body still calls those fields field_8 to field_28.
  • Ghidra keeps the callees — it labels the retain, msgSend and release stubs, the encodeObject:forKey: selector and the key strings (cf__uuid, cf__name). openbinary leaves them as bare addresses, 0x1000fef20 and 0x1001037c8, names its temporaries after registers (r12_1, r13_1) and keeps an unused rax_7 for the release return.

What this doesn't show

  • Binary Ninja finds 12% more functions — 28,372 on locationd against our 25,406. Counting (thunks, stubs, externals) or functions we miss? Unresolved, and their input isn't published.
  • The function counts agree for a reason — these files carry LC_FUNCTION_STARTS, which every tool reads, so the agreement says nothing about discovery on a stripped file. Function-start precision and recall on a stripped copy, against the binary's own starts table, and correct bounds, are planned next.
  • No measure of C correctness — the C is measured only as "a body was produced". No comparison against DWARF prototypes, no recompilation. The mismatched struct field names above are the defect such a measure would catch.
  • The machine wasn't idle — 1-minute load average 4 to 16 during the runs, one radare2 run saw 55. Load only adds time, which is why every figure is the fastest run; the mean would have said 29x. An idle rerun is open.
  • Two inputs — both Mach-O x86_64 slices of macOS daemons. No Linux ELF, no Windows PE, no arm64 slice, no large binary such as vmlinux or chrome.
  • The runs aren't symmetric — Ghidra's time includes JVM start, project creation and save; ours, process start and writing the JSON. Ghidra ran under a 60 s per-function timeout; openbinary's decompiler runs under its own internal budgets, not quoted here.
  • You can't rerun our row — the CLI isn't downloadable. The Ghidra, radare2 and rizin rows are rerunnable by anyone with the two slices.

Run it yourself

Both universal binaries are in the public corpus, analysed as the x86_64 slice. The function counts match the tables: locationd, 25,406 functions, and syspolicyd, 4,174 functions.

# Apple M4 Pro, 12 cores, 24 GB RAM, macOS 26.5 build 25F71# three timed runs per cell, one unmeasured warm-up per tool first, /usr/bin/time -l around the tool process# syspolicyd universal 68718642e44a4eca3e1df85e7501f04167ac555632174a296cc48abad845bf51# syspolicyd x86_64 slice 216cd07e5eaec14046b45574f7d8824da2013da2b836fda81869d40c5f368d85, 1,344,688 bytes# locationd universal 8440da928be4b9e62fdd25294bdebff234ac49d377f7676ca9707636ef79e7bb# locationd x86_64 slice 285b4cc7997eb5e46d289ac01aebd05bf82d5c8eb178a2e4540f77f63e893924, 9,598,000 bytes$ lipo -thin x86_64 locationd -output locationd.x86_64 # openbinary 26.10.8, release build (opt-level 3, thin LTO), rustc 1.98.0# CLI sha256 bf6a9415d2acb7a6a80541d611f6f37f649abea895140828d94a81ca42492b0f$ /usr/bin/time -l openbinary analyze locationd.x86_64 --include metadata,symbols,strings,language_metadata,functions,call_graph,library_functions --deobfuscate off$ /usr/bin/time -l openbinary analyze locationd.x86_64 --include metadata,symbols,strings,language_metadata,functions,call_graph,library_functions,pseudocode --deobfuscate off # Ghidra 12.1.4 PUBLIC, ghidra_12.1.4_PUBLIC_20260921.zip# zip sha256 ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db# Temurin JDK 21.0.12.1+1 aarch64; decompile, sleigh and demangler_gnu built with support/gradle buildNatives# GHIDRA_MAXMEM=8G, default analysers, default headless VM arguments# CountFunctions.java sha256 fea94b4d4185e1086f26fb995ccf00f85ed524fb173df811402259ec2ca38096# DecompileAll.java   sha256 34e8a315bbba80b3c5ac767927a31e8430c4d1d9059d772126788a94850d8204$ /usr/bin/time -l analyzeHeadless proj p -import locationd.x86_64 -deleteProject -postScript CountFunctions.java$ /usr/bin/time -l analyzeHeadless proj p -import locationd.x86_64 -deleteProject -postScript DecompileAll.java # radare2 6.2.5 (336de7d2e6) from source; rizin 0.10.0 (db112e55614b) from source with meson; no decompiler plugin$ /usr/bin/time -l radare2 -q -e scr.color=0 -c "aaa; aflc" locationd.x86_64$ /usr/bin/time -l rizin -q -e scr.color=0 -c "aaa; aflc" locationd.x86_64
Figure 1. The benchmark commands, machine, versions and hashes. Run of 2026-10-19.

Function counts are openbinary's own count, Ghidra's non-external functions, and aflc for radare2 and rizin. Ghidra's decompiler children were sampled every second and added to its figures. Binary Ninja's numbers are from their release post. Their machine: an AMD Ryzen 9 9950X, 96 GB, Ubuntu 26.04, idle. Default headless analysis, full decompilation included. Their input hashes and macOS build aren't published.

Anyone with the two slices can rerun the Ghidra, radare2 and rizin rows. For ours, check the two public pages above, or upload a binary of your own.