The same Mandelbrot workload. The same machine. What changes is how much of the M4 Max you actually use — SIMD lanes, all 14 CPU cores, algorithmic symmetry, the 40-core GPU, both engines on one frame at once, and finally the WWDC 2026 Metal 4 command model. Every step measured, verified, and documented below.
Peak performance on Apple Silicon isn't one trick — it's refusing to leave any of the M4 Max's execution resources idle.
Four float32 lanes per instruction on every core, with fused multiply-add and branch-free lane masking.
dispatch_apply fans work across performance and efficiency cores with work-stealing — no thread-pool code.
Thousands of threads for embarrassingly parallel math, shaders compiled at runtime from source.
One physical memory. The GPU writes, the CPU reads the very same bytes — no PCIe transfer, no staging buffers.
The repository benchmarks SQL engines (recursive CTEs!), Python and NumPy against native code. Same fractal, same escape semantics — from four minutes to about a sixth of a millisecond, an over-1.5-million-fold span.
Escape-time Mandelbrot: iterate z = z² + c per pixel until |z|² > 4.
Simple to state, brutal to make fast — the loop is a serial dependency chain with unpredictable exit.
A dylib built at first import with clang++ -O3 -mcpu=apple-m4, called from Python via
ctypes. The compiler schedules for the M4's real pipeline widths and latencies. Build cost paid once, outside the timed path.
The escape loop runs on float32x4_t vectors. Escaped lanes are masked out branch-free;
iteration counts accumulate by subtracting the all-ones comparison mask. float32 over float64 doubles
the lanes — at this zoom the pixel pitch is ~10⁴ ulp of f32, so precision is ample (verified: 99.8 % of pixels within ±1 iteration of the f64 reference).
float32x4_t zr2 = vmulq_f32(zr, zr), zi2 = vmulq_f32(zi, zi);
active = vandq_u32(active, vcleq_f32(vaddq_f32(zr2, zi2), four));
count = vsubq_u32(count, active); // +1 where lane still alive
zi = vmlaq_f32(ci, vmulq_f32(two, zr), zi); // fused 2·zr·zi + ci
dispatch_apply spreads rows across P- and E-cores with built-in work-stealing.
Two profiler-enforced findings: QOS_USER_INTERACTIVE was 4× worse than USER_INITIATED for this
workload, and the pool is warmed by a tiny dispatch at import so the first timed call doesn't pay
thread-creation latency.
Points inside the main cardioid and period-2 bulb never escape — and they're the most expensive pixels, burning all 256 iterations. Both regions have closed-form membership tests: a handful of FLOPs replaces 256 iterations, evaluated vectorized before the loop.
// q·(q + (cr − ¼)) ≤ ¼·ci² → inside main cardioid
float32x4_t q = vmlaq_f32(ci2, crm, crm);
uint32x4_t in_set = vcleq_f32(vmulq_f32(q, vaddq_f32(q, crm)),
vmulq_f32(quarter, ci2));
z = z² + c is a serial dependency chain — one 4-lane chain can't fill the M4's SIMD pipes.
Interleaving two independent vector groups (8 pixels) keeps the FMA units busy while each chain waits
on its own latency. Measured: G=1 → 0.71 ms, G=2 → 0.39 ms, G=3 → 0.46 ms (register pressure wins).
The viewport is symmetric about the real axis, and conj(z² + c) = conj(z)² + conj(c) — with
IEEE negation being exact, escape counts are bit-identical for mirrored rows. Compute the top half,
memcpy the rest. The best optimization in the whole project is a line of mathematics.
The horizontal reduce (vmaxvq_u32) + branch asking "any lane alive?" serializes the loop.
It now runs every 4th iteration. Per-lane masking stays exact — dead lanes idle through up to 3 wasted
iterations, and inf/NaN comparisons can never resurrect them.
The MSL kernel ships as a source string — newLibraryWithSource compiles it at import, no
offline toolchain. One thread per top-half pixel; each writes its pixel and the mirrored one.
The cardioid early-out returns whole 32-wide SIMD groups early in bulk regions.
kernel void mandel(device ushort *out [[buffer(0)]],
constant uint4 &p [[buffer(1)]],
uint2 gid [[thread_position_in_grid]])
{
// … escape loop …
out[gid.y * w + gid.x] = (ushort)it;
out[(h - 1 - gid.y) * w + gid.x] = (ushort)it; // mirror row
}
The GPU writes into a StorageModeShared buffer; Python receives a numpy view of the same
physical memory. No 2.2 MB memcpy, no staging, hazard tracking off — waitUntilCompleted
is the only synchronization. This is the Apple Silicon advantage discrete GPUs can't match at small sizes.
Swept 32×4 / 32×8 / 32×16 / 32×32 threadgroups: flat through 16, clear regression at 32 (occupancy). Shipped 32×8 = 256 threads per group, matching Apple's guidance — but only after the sweep confirmed it.
Unified memory's endgame. The Metal kernel takes ~56 % of the rows with an async commit;
while the GPU renders, a clang ext_vector float8 kernel computes the remaining rows on all
14 CPU cores — into the same shared MTLBuffer. Disjoint rows, hazard tracking off, one
waitUntilCompleted. The split is calibrated once at import. A discrete GPU would need a
PCIe round-trip here; this win is pure Apple Silicon architecture.
The one WWDC 2026 feature that fits a divergent escape loop — not tensors, not the Neural
Accelerators (those are M5, and this is matmul hardware anyway). Metal 4 replaces the
per-dispatch bookkeeping with a pre-baked MTL4ArgumentTable (resources bound by raw
GPU address), an explicit MTLResidencySet, and a reusable
MTL4CommandAllocator/MTL4CommandBuffer committed on an
MTL4CommandQueue. At this size the ~0.26 ms fixed submit latency dominates, so cutting
it matters: GPU-only drops ~6 %, but in the hybrid the cheaper submit lets the split shift to
76 % GPU with a tighter async overlap — a robust ~31 % win, 0.24 → 0.15 ms
(faster on 90 % of paired samples). Same kernel, bit-identical output; only the submission path
changed.
A Metal dispatch carries a fixed ~0.2 ms command-buffer cost. At sub-millisecond workloads the 14-core CPU is nearly even; scale up and the 40-core GPU runs away.
| Workload | C++ NEON · 14 cores | Metal · 40-core GPU | GPU advantage |
|---|---|---|---|
| 1400×800 (1.1 Mpx) | 0.52 ms | 0.34 ms | 1.5× |
| 2800×1600 (4.5 Mpx) | 1.38 ms | 0.61 ms | 2.2× |
| 5600×3200 (18 Mpx) | 4.94 ms | 1.58 ms | 3.1× |
| 11200×6400 (72 Mpx) | 24.5 ms | 1.87 ms | 13.1× |
| 1400×800 @ 4096 iter | 3.05 ms | 0.80 ms | 3.8× |
Same SIMD strategy, same early-outs, same symmetry, same escape semantics — reimplemented in seventy-three languages to separate language speed from algorithm speed.
ext_vector_type beat
hand-picked NEON intrinsics. The top seven all compile through LLVM; hand-written assembly
loses ~55 % to the best of them. WebAssembly beats plain C. Vector-API JVMs cluster
within ~4× of native. And the COBOL entry only became viable after swapping floats for Q28
fixed-point — its float math routes through a decimal library. Python's escape hatches reach
the compiled tier: Cython 1.53 ms, Numba 2.23 ms, Pythran 1.4 ms — ~2 000× past the interpreter
they extend. Gleam runs the same BEAM as Erlang 2× faster — typed, monomorphized code boxes less.
And two languages with no float type at all still finish: Rexx in decimal string arithmetic (1.7 s),
Bash in Q26 fixed-point shell integers (5.6 s) — both still ~10× ahead of SQLite.
float[<8>] → NEON) at
0.80 ms, ISPC (SPMD, neon-i32x8) at 0.91 ms, Halide (schedule-DSL, JIT) at
7.16 ms. None came close — all three share one wound: a vector-group escape loop can't retire a
lane the moment it escapes, so every 8-wide gang iterates to max_iter for its slowest
pixel. The hand-written NEON kernel wins because it amortizes the escape check instead. The CPU
crown holds at 0.34 ms — but the "even faster" hunt did land elsewhere: the WWDC 2026 Metal 4
command model cut the hybrid from 0.24 to 0.15 ms (see step 12). Language choice couldn't beat the
kernel; the submission API could.
Each card links to the exact file in the GitHub repository, with an honest note on what limits that language on this workload.
Performance engineering is empirical. These all sounded right and measured wrong.
"Highest priority = fastest," right? Median regressed 4× vs USER_INITIATED. The scheduler knows things you don't.
More chains in flight should hide more latency — but past G=2, register pressure and spills beat the gains.
Maxing threadgroup size cost measurable occupancy. 256 threads/group won the sweep.
Until symmetry halved the work, the kernel was overhead-bound — unroll factors measured as pure noise. Fix the algorithm before polishing the loop.
Bit-exactness across restructures. v2's top half is bit-identical to v1; every ILP variant produced identical output before timing was even considered.
Accuracy vs float64. Every escaped pixel compared against the f64 reference: ≥ 99.76 % within ±1 iteration at every step (CPU f32: 99.81 %, GPU fast-math: 99.77 %). The differences live on chaotic boundary filaments.
Identical semantics. Same escape convention as the repo's Python and SQL implementations — count = iterations survived before |z|² > 4.
Honest timing. Best and median of 15–31 runs for steady state; fresh-process single calls for what a benchmark harness actually sees; contenders re-measured on an idle machine.
Everything is open source — the kernels, the harness, the charts, this page.
git clone https://github.com/jirakj/sql-mandelbrot-benchmark
cd sql-mandelbrot-benchmark
# The whole comparison, every engine + all 73 languages:
uv run python main.py
Every single test runs the same way — one file per implementation,
named <lang>brot.py. Running it clones nothing else, builds itself, then measures:
the wrapper compiles the native kernel (or spawns the interpreter/VM pool), runs a warm-up, computes
the frame, and writes images/<lang>brot.png as proof. No per-language setup, no build
scripts to read — the Python file is the build system.
# The record and the crowns
uv run python hybrid4brot.py # Hybrid, Metal 4 command model — 0.15 ms (record)
uv run python hybridbrot.py # Hybrid, Metal 3 — 0.24 ms
uv run python metalbrot.py # Metal GPU — 0.30 ms
uv run python objcbrot.py # Objective-C ext_vector — 0.34 ms (fastest CPU)
# Any language in the shootout — same pattern:
uv run python cppbrot.py # C++ NEON 0.39 ms
uv run python ispcbrot.py # ISPC SPMD 0.91 ms
uv run python tsbrot.py # TypeScript 2.34 ms
uv run python haskellbrot.py # Haskell 1.94 ms
uv run python cobolbrot.py # COBOL (Q28 fixed) 201 ms
uv run python bashbrot.py # Bash (Q26 fixed) 5.6 s
# …73 in total — swap in any name from the cards above.
Toolchains the native entries need (ispc, halide, gambit, gforth, …) install via Homebrew; each wrapper's docstring names its compiler. Full write-up with every measurement: OPTIMIZATIONS.md