Menu
Version 0.0.3
Performance
v0.0.3 is 1.2–3.2× faster than v0.0.2 per sample, and 1.7–5.5× with mini-batching. Most of the margin on wide layers comes from one compiler flag.
Every figure on this page comes from 0.0.3/bench/run_bench.sh on the current source. The previous numbers in the README were v0.0.2-vs-v0.0.1 and had been carried forward unmeasured; they are gone.
Method
Both engines are driven through one shared harness, bench/bench.cpp, selected by -DBENCH_V2. The timing loop, the input data and the architectures are therefore identical across versions by construction rather than by inspection.
| Machine | Apple M5, 10 cores, macOS; Apple clang 21.0.0 |
| Threads | One. #pragma omp simd is instruction-level parallelism; core/ contains no parallel regions. |
| Unit | Microseconds per sample — one forward pass, one backward pass, one weight update. |
| Estimator | Minimum of nine trials. Interference only ever adds time, so the fastest run is the least contaminated estimate. |
| Data | 256 samples from a fixed LCG, identical in both builds. |
At batch size 1 both engines do the same work per sample. v0.0.3 additionally zeroes its gradient buffers on every update — a real cost of the batching architecture, counted here rather than excused. The median and the minimum agree to within 0.06× everywhere, so the choice of estimator moves no conclusion; a clean re-run drifted 1.0 % at the median.
Speedup Over v0.0.2
-O2, no OpenMP — single precision and the structural changes alone:
| Architecture | Params | v0.0.2 | v0.0.3 | Speedup |
|---|---|---|---|---|
2→8×2→1 | 105 | 0.131 | 0.111 | 1.16× |
20→32×2→3 | 1,827 | 1.040 | 0.648 | 1.63× |
64→128×3→10 | 42,634 | 37.439 | 17.407 | 2.15× |
784→128×2→10 | 118,282 | 85.407 | 64.357 | 1.33× |
-O2 -fopenmp-simd, which is what makes the directives apply:
| Architecture | v0.0.2 | v0.0.3 | Speedup | v0.0.3 @ batch 32 | Speedup |
|---|---|---|---|---|---|
2→8×2→1 | 0.139 | 0.103 | 1.35× | 0.078 | 1.78× |
20→32×2→3 | 1.062 | 0.564 | 1.88× | 0.413 | 2.57× |
64→128×3→10 | 37.517 | 12.132 | 3.09× | 7.763 | 4.83× |
784→128×2→10 | 85.311 | 26.589 | 3.21× | 15.648 | 5.45× |
-O3 made no measurable difference over -O2 at any size. It is in the matrix and in bench/results.csv, but there is nothing to report from it.
Cost Per Sample
The OpenMP Flag Is Most Of It
Apple clang rejects -fopenmp, which is the flag usually named, and an earlier revision of these docs concluded that v0.0.3’s vectorisation was therefore inert on macOS. It is not. Apple clang accepts -fopenmp-simd, which enables exactly the SIMD directives and needs no runtime library — and core/ uses nothing else.
clang++ -std=c++20 -O2 -fopenmp-simd main.cpp -o main # Apple clang
g++-14 -std=c++20 -O3 -fopenmp main.cpp -o main # GCC, full OpenMP
Turning it on found two bugs
Unrecognised #pragma omp directives are discarded in total silence — -Wall -Wextra says nothing about them. Two defects had been sitting behind that silence:
- A malformed reduction.
softmaxCrossEntropyDerivativecarriedreduction(+:vectorScratchPad[:size])over a loop that overwrites the array rather than accumulating into it. An array-section reduction gives each lane a private copy initialised to the identity and sums them at the end — the wrong answer. Building the test suite with the flag failed the fused-softmax test immediately. hubercomputing indouble.0.5rather than0.5fpromoted the whole expression in an engine that isfloateverywhere else, which is also why clang refused to vectorise the loop.
Both are fixed, and tests/run_tests.sh --simd builds with the flag so neither can return unnoticed. With it on, core/ compiles warning-free.
Reproducing
cd 0.0.3
./bench/run_bench.sh # full matrix, CSV on stdout
./bench/run_bench.sh > out.csv
The runner sweeps -O2 and -O3 against OpenMP on and off, building both engines under each combination. Raw output for the run on this page is checked in at 0.0.3/bench/results.csv. The charts are regenerated from it by docs/make_bench_figures.py.
What Is Not Measured
- v0.0.1. The old README compared v0.0.2 against v0.0.1 and claimed ~2.4×. That column was dropped rather than carried forward: v0.0.1 is a single translation unit with a different API, and building a fair harness for it was not part of this work.
- Accuracy per unit time. These are throughput figures. Single precision changes the arithmetic, so a faster update is not automatically a faster path to a given loss — nothing here measures that.
- Multi-threading. Everything is single-threaded, and there is no
parallel foranywhere incore/. - Memory. Peak resident size and allocation counts are not tracked. v0.0.2’s zero-allocation assertion was not ported; the flat layout it guarded is unchanged, but nothing currently asserts it.