Reading Assembly

Part 3 · Reading compiler output

Verifying "zero-cost"

Reading Rust told you iterator chains compile to the same loop. This is where you check, and the real answer is more interesting than yes or no.

40 minauthentic verificationcontrasting casesPOE
What this lesson is trying to break (3)
  • "Zero-cost" is a slogan you have to take on trust.
  • Identical performance means byte-identical output.
  • High-level abstractions must cost something.

If you worked through Reading Rust, you were told repeatedly that iterator chains are “zero-cost” and that they compile to the same loop you would have written. You were asked to take it on trust.

Here is the check. The numbers below were measured with rustc 1.96 at -C opt-level=2 — the same version you have installed locally — so you should reproduce them exactly.

The two versions

#[no_mangle]
pub extern "C" fn f(p: *const i32, n: usize) -> i32 {
    let v = unsafe { std::slice::from_raw_parts(p, n) };
    v.iter().filter(|&&x| x > 0).map(|&x| x * 2).sum()
}
#[no_mangle]
pub extern "C" fn f(p: *const i32, n: usize) -> i32 {
    let v = unsafe { std::slice::from_raw_parts(p, n) };
    let mut s = 0i32;
    let mut i = 0;
    while i < v.len() {
        if v[i] > 0 { s += v[i] * 2; }
        i += 1;
    }
    s
}

One is three combinators. The other is an index loop with a bounds-checked subscript on every access.

The #[no_mangle] pub extern "C" wrapper is there for a practical reason worth knowing: a plain pub fn in a library crate does not reliably produce a standalone function in Compiler Explorer’s output — it can be optimised away or have its symbol filtered. Forcing external C linkage guarantees a real, ABI-conforming body to read. This is 3.1’s warning in practice.

Predict firstcommit in writing, then look

Commit numbers for both before compiling. Also predict whether the bounds checks in the second version survive.

The interesting part is “not identical”

A weaker course would have wanted byte-identical output and called this a partial result. It is the opposite: the difference proves you now know how to read.

Register allocation is arbitrary among equivalent choices. Nothing distinguishes zeroing eax from zeroing edx here — the register is a scratch accumulator and either works. The compiler reached the same destination by two paths that happen to label things differently.

This is precisely the distinction the course has been drilling since 0.1: incidental versus essential. Register names, operand order, and instruction scheduling are incidental. Instruction count, memory traffic, dependency structure and vectorisation are essential.

A reader who cannot tell those apart concludes “the outputs are different, so the abstraction costs something” — and is wrong. A reader who can, concludes “identical work, different arbitrary labels” — and is right. That judgement is what fifteen lessons bought you.

Why Rust wins the aliasing argument

There is a second, less-celebrated result here. Write the C equivalent:

int f(const int *p, size_t n) {
    int s = 0;
    for (size_t i = 0; i < n; i++) if (p[i] > 0) s += p[i] * 2;
    return s;
}

This one reads through a single pointer so it vectorises fine. But the moment a C function has an output pointer as well as an input pointer, the compiler must emit a runtime alias check and a scalar fallback (3.4), because nothing in C forbids the caller passing overlapping ranges.

Rust’s &mut is an exclusivity guarantee. rustc emits LLVM’s noalias attribute on those pointers, so the compiler knows there is no overlap without checking. No alias check, no fallback path, smaller and faster code.

So the borrow checker is not only a safety mechanism — it is an optimisation input that C can only obtain by hand-annotating restrict and hoping the annotation is true. That is the concrete cash value of everything Part 2 of the Rust course put you through, and you can now see it in the instruction stream rather than believe it.

Where zero-cost stops being true

Being able to name the exceptions is what makes the claim credible rather than promotional:

  • dyn Trait — a real indirect call through a vtable, which cannot be inlined. That is not zero-cost and was never claimed to be.
  • Rc/Arc clones — real refcount traffic; Arc uses atomics, which are genuinely expensive under contention.
  • Bounds checks that cannot be proven away — indexing by a value the compiler cannot bound leaves a real check. Iterators avoid this by construction, which is an argument for the abstraction rather than against it.
  • RefCell — a runtime flag check on every borrow.
  • Iterators over non-trivial closures that fail to inline, which is rare but happens with very large bodies.

The honest formulation, which is the one to use in a discussion: Rust’s iterator and generic abstractions are zero-cost; its runtime-polymorphism and shared-ownership abstractions are not, and are not sold as such.

Explain it to yourselfwrite it — thinking it does not count

Pick one abstraction you currently take on trust — in Rust, C++, or anywhere with a compiler — and design the experiment that would check it.

Be specific: what two versions would you compile, what flags, and what would you count in the output to decide? Then run it.

If your two versions differ in a way that is not the thing you are testing, the experiment is invalid. Say how you would know.

Part 3 checkpoint

  1. Name three ways Compiler Explorer differs from your build. (3.1)
  2. Why is -O0 readable and misleading at once? (3.2)
  3. Why is inlining the most valuable optimisation? (3.3)
  4. A multiply by a large odd constant plus shifts — what was it? (3.3)
  5. Name three reasons a loop refuses to vectorise. (3.4)
  6. Why does field order change sizeof? (3.5)
  7. What did the iterator-versus-loop comparison actually establish? (3.6)

You can now read optimised output and check a claim about it. Part 4 breaks all four fictions, and it is the hardest part of the course.

Next: 4.1 — The listing is a dependency graph.

In the wildgo read the real thing now, while it is fresh
  • The exact comparison, in Compiler ExplorerPaste both versions from this lesson side by side and use the diff view. Doing it yourself is the point of the lesson — the numbers here were measured with rustc 1.96, the same version on your machine, so you should reproduce them exactly.
Scheduled for recall3 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course — not grouped by topic. Sorting out which idea applies is the part that transfers.