m03 · Burst · reading · 9 min
Reading the ASM — proving it, not hoping it
The only source of truth
Every claim in this module so far — it vectorized, the branch broke it, the math type became one instruction — is checkable, and there is exactly one place to check it: the assembly Burst generated. Unity ships the Burst Inspector (Jobs ▸ Burst ▸ Open Inspector), which shows you, for any Burst-compiled method, the actual machine instructions produced for your target CPU. Everything else is inference; this is the artifact.
Learning to read it doesn’t require deep assembly fluency — it requires
recognizing a few patterns. Vectorized floating-point math shows up as
instructions on the wide registers: vmulps, vaddps, vfmadd... on
ymm/xmm registers (the p is “packed” — multiple values per
instruction). Scalar fallback shows the same operations without the pack:
vmulss, vaddss (the s is “scalar” — one value). If you expected a
vectorized loop and the inner block is full of ...ss scalar ops, the
compiler didn’t vectorize it, and now you know to hunt for the branch,
dependency, or opaque call that stopped it. The Inspector turns “I
think it’s fast” into “I can see the packed instructions or I can’t.”
The benchmark that lies to you
Now the failure that has fooled nearly everyone who benchmarks Burst once. You write a loop that does a million multiplies, time it, and it reports a number so fast it implies the CPU did billions of operations in nanoseconds. You did not discover a miracle. You measured nothing, because the optimizer deleted your loop.
Here’s the mechanism, and it follows directly from what an optimizer is allowed to do. If a loop computes a result that is never used — never read, never stored anywhere observable, never returned — then the loop has no effect on the program’s output, and a correct optimizer is free to delete it entirely. This is dead code elimination (DCE), and it’s not a bug; it’s the compiler correctly noticing that unobserved work is work it doesn’t have to do. Burst, being aggressive, does this reliably. Your “benchmark” of unused arithmetic compiles to an empty loop, and you time the empty loop.
The defeat for DCE is to make the result observable, so the compiler can’t prove the work is dead:
// WRONG: sum is never used → the whole loop is deleted → you time nothing.
float sum = 0f;
for (int i = 0; i < n; i++) sum += data[i] * 2f;
// (nothing reads sum)
// RIGHT: write the result somewhere the compiler must preserve.
float sum = 0f;
for (int i = 0; i < n; i++) sum += data[i] * 2f;
output[0] = sum; // now the loop has an observable effect and survives
Storing to a NativeArray the compiler can’t see through, returning the
value, or accumulating into an output the caller reads — any of these
anchors the work as observable. The rule generalizes: a benchmark only
measures work whose result escapes the optimizer’s view.
Benchmarking honestly, beyond DCE
Even with DCE defeated, several things move the number, and an honest benchmark controls them:
- Warmup. The first execution pays one-time costs — JIT/Burst compilation of the method, cold caches, first-touch page faults. Timing it measures startup, not steady state. Run the workload several times untimed, then measure, and report a stable central value (median over many runs), not a single sample.
- Compilation mode. Burst in the Editor with safety checks and debugging on is not Burst in a release player. Leak detection, bounds checks, and synchronous-compilation stalls all inflate Editor numbers. A number that decides an architecture choice should come from a release build, or at least Burst with safety checks off — and you should know which you measured.
- Measure the work, not the scaffolding. Timing a loop that includes
allocation, container setup, or a
Debug.Logmeasures those too. Isolate the arithmetic you’re actually asking about, and make sure the entity count is large enough that the per-run fixed costs are noise against the work.
None of this is Burst-specific ceremony; it’s what separates a
measurement from a vibe. The discipline is the same one the labs have
enforced since the
What this buys you
You can now prove what the last four lessons claimed: open the
Inspector and see whether your loop vectorized, and write a benchmark
that measures real, observed work in steady state rather than an empty
loop in a cold Editor. That closes the Burst module — you understand the
compiler, what it does to your loops, the aliasing contract that unlocks
it, the math types that make SIMD explicit, and how to verify all of it.
The lab makes you do exactly that: measure a vectorized loop against a
de-vectorized one, defeat DCE, and read the assembly to confirm the
vmulps is really there. After Burst comes the other half of DOTS
performance — running these compiled loops across many cores at once —
the Jobs system.