25x faster than Ghidra

openbinary decompiles all 25,406 functions of locationd in 12 seconds, where Ghidra takes five minutes. Where the time goes, and what that changes.

Jimmi Andersen20 Oct 2026 · 14 min read · engine 26.10.8

A decompiler turns a compiled program back into readable source code, one function at a time. openbinary 26.10.8 decompiles all 25,406 functions in locationd, the background program macOS runs for location services, in 12 seconds, and Ghidra 12.1.4, the standard free decompiler, takes 303 seconds on the same file and laptop.1

Time to decompile locationd, all 25,406 functions
openbinary·12 seconds
25x faster
Ghidra·5 min 3 s
0 s1 min2 min3 min4 min5 min

At five minutes a file, an analyst decompiles the one file under suspicion. At twelve seconds, every binary in a release can be decompiled before anyone suspects it.

Results

We measured two programs from macOS: locationd, 9.6 MB and 25,406 functions, and syspolicyd, which decides whether software may run, 1.3 MB and 4,174 functions. Ghidra, radare2 and rizin ran on our Mac. Binary Ninja, a paid decompiler, published its own numbers from a much larger machine, which we show but did not measure.2 Analyse is the step before decompiling, finding the functions and the calls between them. radare2 and rizin do not decompile, so their Decompile cells are empty; they do find functions, so they appear in the Analyse and Functions found rows and in the function-finding test.

locationd, 25,406 functions, 9.6 MB

openbinary26.10.8Ghidra12.1.4Binary Ninja*6.0radare26.2.5rizin0.10.0
Decompilea C body for every function12.00s25x vs Ghidra302.73s41.4sn/an/a
Analysefinding the functions and the calls, no C4.30s29x vs Ghidra126.63sn/a79.15s44.93s
Processor timeall cores added up28.9s14x vs Ghidra409.3sn/an/an/a
Memorypeak1.82GB2.1x vs Ghidra3.83GB3.62GBn/an/a
Work done by the toolbillions of processor instructions executed42112x vs Ghidra4,9412,790n/an/a
Functions foundthe four tools on this Mac agree within 0.5%25,40625,53128,37225,44225,424
*Binary Ninja's numbers are their own run, on their own machine, not our measurement. Each timed cell for the tools we ran is the fastest of three runs; the slowest were 427 s for Ghidra and 12.3 s for openbinary.

syspolicyd, 4,174 functions, 1.3 MB

openbinary26.10.8Ghidra12.1.4Binary Ninja*6.0radare26.2.5rizin0.10.0
Decompilea C body for every function1.73s78x vs Ghidra134.97s7.0sn/an/a
Analysefinding the functions and the calls, no C0.75s58x vs Ghidra43.78sn/a7.18s3.18s
Functions foundthe four tools on this Mac agree within 0.5%4,1744,2274,2154,1804,174
*Binary Ninja's numbers are their own run, on their own machine, not our measurement. Each timed cell for the tools we ran is the fastest of three runs; the slowest were 157 s for Ghidra and 1.8 s for openbinary.

The smaller file widens the gap to 78x, since Ghidra is written in Java and sets up a project database before it reads the file, a fixed cost that dominates on a small file. Both binaries are public: locationd, syspolicyd.

Flutter engine, 40,726 functions, 21 MB

The third file is a Linux library, libflutter_linux_gtk.so, the engine behind Flutter desktop apps, written in C++, with its debug information removed and its symbol table kept.

openbinary26.10.8Ghidra12.1.4radare26.2.5rizin0.10.0
Decompilea C body for every function41.7s37x vs Ghidra1547.7sn/an/a
Analysefinding the functions and the calls, no C16.4s31x vs Ghidra510.7s115.1s204.4s
Processor timeall cores added up63s36x vs Ghidra2288sn/an/a
Memorypeak3.79GB2.1x vs Ghidra7.78GBn/an/a
Work done by the toolbillions of processor instructions executed87713x vs Ghidra11,030n/an/a
Functions foundthe four tools agree within 2%40,72641,38741,11941,594
Ghidra ran once on this file, on a machine busy with other work; the other three tools ran three times and the fastest run is shown.

searchpartyd, 56,718 functions, 22 MB

The largest macOS file we timed is searchpartyd, the background program behind Find My, with twice the code of locationd.

openbinary26.10.8Ghidra12.1.4radare26.2.5rizin0.10.0
Decompilea C body for every function36.8s311x vs Ghidra11434.2sn/an/a
Analysefinding the functions and the calls, no C17.4s129x vs Ghidra2236.3s160.4s66.7s
Processor timeall cores added up77s227x vs Ghidra17554sn/an/a
Memorypeak3.60GB1.3x vs Ghidra4.84GBn/an/a
Work done by the toolbillions of processor instructions executed1,09912x vs Ghidra13,038n/an/a
Functions foundthe four tools agree within 0.6%56,71856,82557,00857,040
Ghidra ran once on this file: 37 minutes for the analysis pass and 3.2 hours for the decompile, with the machine's load between 20 and 68 throughout, so its wall times carry that load; its instruction count, which load does not move, is 12x ours. The other three tools ran three times and the fastest run is shown.

dsymutil, 43,056 functions, 50 MB

The fifth file is dsymutil, the tool Xcode uses to build debug-symbol bundles, 50 MB of C++ from the LLVM project. It is the one file where openbinary uses more memory than Ghidra, 8.9 GB against 6.4 GB at peak, and one of its functions, with 2,401 basic blocks and 27 switch tables, takes 18 of openbinary's analysis seconds on its own.

openbinary26.10.8Ghidra12.1.4radare26.2.5rizin0.10.0
Decompilea C body for every function256.3s18x vs Ghidra4548.6sn/an/a
Analysefinding the functions and the calls, no C47.1s186x vs Ghidra8755.2s444.6s479.1s
Processor timeall cores added up385s20x vs Ghidra7604sn/an/a
Memorypeak8.93GB1.4x more6.41GBn/an/a
Work done by the toolbillions of processor instructions executed5,8916.9x vs Ghidra40,544n/an/a
Functions foundthree agree within 2%, rizin 9% more43,05642,99343,75547,082
Ghidra ran once on this file, its analysis pass with the machine's load near 20 and its decompile near 5; the other three tools ran three times with the load under 6 and the fastest run is shown.

Where the time goes

Two effects account for the gap. openbinary executes 12x fewer instructions than Ghidra on the same file, so it spends 14x less processor time, and it keeps on average 2.4 cores busy against Ghidra's 1.4. Instruction count is the most stable measure: it moved under 0.1% between runs, while Ghidra's wall time moved by 40% because the laptop was busy. Memory follows the same shape, 1.8 GB against 3.8 GB at peak.

Processor time14xless than Ghidra
Instructions executed12xfewer than Ghidra
Memory2.1xless than Ghidra
Read the fileonce, no import step
Find the functionsand what each one touches
Translate Intel, ARM and the restinto one internal language
Turn every function into Call functions at once, one per core
Write one documentthen exit
openbinary starts no Java virtual machine and builds no project database, so the file goes in and one document comes out.

Every function becomes an independent job that any core can take, and producing the C accounts for two thirds of the time, finding the functions for the rest.

Finding the functions

A tool can be faster because it finds fewer functions, so we compared the counts: the four tools on this Mac agree within 0.5% on each file. Ghidra hit its 60-second limit on five functions, four in locationd and one in syspolicyd, and produced no C for those, which openbinary did.

macOS programs list their function addresses inside the file. A Linux program leaves the compiler with a symbol table, a list of every function's name and address, and stripping removes that list before the program ships, so a decompiler has to find the function starts on its own. We stripped three Linux files and measured the share of the table's functions each tool found, and how many it reported where the table has none.3

openbinary26.10.8Ghidra12.1.4radare26.2.5rizin0.10.0
Ghidra's own decompilera Linux program, C++, 8,251 functions100%2 false positives100%2,260 false positives75%38%
localsend-clia Linux program, Rust, 14,547 functions100%3 false positives95%892 false positives56%145 false positives59%1,251 false positives
Flutter enginea Linux library, C++, 39,889 functions100%5 false positives100%14 false positives47%385 false positives46%539 false positives
Share of the symbol table's functions each tool found at the exact address on the stripped file. A false positive is a function reported at an address where the symbol table has none.

openbinary found every function in all three files, with ten false positives across 62,687 functions. Ghidra matched it on the two C++ files and found 95% on the Rust one, with far more false positives. The stripped files keep the unwind tables, a crash-handling structure that marks function starts, and openbinary relies on them: with those removed as well, which no normal build does, it finds only a third in the first file.

Side by side

A small function from syspolicyd, picked by rule rather than by hand.4

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 tools: produce the same five encode calls in the same order.
  • openbinary: keeps the method's real name and reads its fields by name, self->uuid and self->name, while the struct declaration above the body still names them field_8 to field_28, which is a bug on our side.
  • Ghidra: keeps the names of the called helpers, objc_msgSend among them, and the text keys such as uuid, where openbinary shows numeric addresses.

What it changes

At this speed the unit of work is a release, not a file.

  • Coverage: every finding and every attack path is computed over all 25,406 functions, rather than the handful an analyst opened.
  • Scale: a firmware image of hundreds of binaries takes minutes, so every release is a batch job rather than a project.
  • Automation: an AI agent or a CI check can wait twelve seconds inside one step, and neither can wait five minutes.

What we don't know

Binary Ninja reports 28,372 functions on locationd, 12% more than we find, and their input is not published, so we cannot tell counting differences from functions we miss. Produced is not the same as correct: we never checked the C against debug information or recompiled it, and the field_8 names above are the defect such a check would catch. Five files is still a small sample: three macOS programs and two Linux files, all x86_64, no Windows binary, and nothing the size of a browser. Ghidra ran once on the three larger files, so those multipliers carry one run's noise and, on searchpartyd, a busy machine.

What's next

The C is produced but not verified. Next are a Windows binary, a file the size of a browser, and a correctness check of the C. You can upload your own binary.

References

  1. Run of 2026-10-19 on an Apple M4 Pro, 12 cores, 24 GB, macOS 26.5, with the laptop busy with other work: 1-minute load between 4 and 16 throughout. Each cell is three timed runs after one unmeasured warm-up, /usr/bin/time -l around the tool process, Ghidra's decompiler child processes sampled every second and added to its CPU, memory and instruction figures. We report each tool's fastest run: load only adds time, so the fastest run is the closest to a tool's true cost; the slowest run stands under each table. Tools: openbinary 26.10.8 release build, CLI sha256 bf6a9415d2acb7a6a80541d611f6f37f649abea895140828d94a81ca42492b0f; Ghidra 12.1.4 PUBLIC on Temurin JDK 21.0.12.1+1 aarch64 with the native decompiler built, GHIDRA_MAXMEM=8G, default analysers, a 60 s per-function decompile timeout; radare2 6.2.5 and rizin 0.10.0 built from source, aaa analysis, no decompiler plugin; Binary Ninja 6.0 from their published figures only. Function counts are openbinary's own, Ghidra's non-external functions, and aflc for radare2 and rizin. Files: locationd x86_64 slice sha256 285b4cc7997eb5e46d289ac01aebd05bf82d5c8eb178a2e4540f77f63e893924 (9,598,000 bytes) of universal 8440da928be4b9e62fdd25294bdebff234ac49d377f7676ca9707636ef79e7bb; syspolicyd x86_64 slice 216cd07e5eaec14046b45574f7d8824da2013da2b836fda81869d40c5f368d85 (1,344,688 bytes) of universal 68718642e44a4eca3e1df85e7501f04167ac555632174a296cc48abad845bf51. ↩

  2. Vector 35, Binary Ninja 6.0 release post: an AMD Ryzen 9 9950X with 96 GB, Ubuntu 26.04, idle, default headless analysis with full decompilation. Their input hashes and macOS build are not published. ↩

  3. Ground truth is every FUNC symbol in the .symtab that lies in .text, read with readelf, from the unstripped file; the test copies were made with objcopy --strip-all, which keeps .eh_frame. A start counts as found only at the exact address. Function lists came from openbinary's JSON, a Ghidra post-script printing each function's entry point (rebased for position-independent files), and aflj after aaa in radare2 and rizin. The files: Ghidra 12.1.4's own decompile binary (GCC 8.5, 8,251 functions), localsend-cli (Rust 1.97, 14,547) and libflutter_linux_gtk.so from the LocalSend Linux build (39,889, of which 216 remain exported after stripping). ↩

  4. 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. ↩