Aug 3, 2026

[benchmark] how `void DoNotOptimize(Tp const& value)` works under the hood

Reference:
How does Google's `DoNotOptimize()` function enforce statement ordering
[gcc] Constraint Modifier Characters
[google menchmark] void DoNotOptimize()

code by example:
https://godbolt.org/z/EzTr4afEM


How Google's DoNotOptimize Enforces Statement Ordering

This document summarizes the StackOverflow thread How does Google's DoNotOptimize function enforce statement ordering?, focusing in detail on the accepted answer by Peter Cordes (answer 69288151).


🧩 Summary of Ordering Mechanics

Mechanism Targeted Ordering Implementation Compiler's Interpretation
Opaque "memory" Clobber Ordering between DoNotOptimize() and external calls (e.g., time()) asm volatile("" : ... : : "memory") Treated as an opaque, non-inline function call that can read/write any globally-reachable memory, stopping compile-time reordering.
Data Dependency ("+r,m") Ordering of the benchmarked computation itself (input -> output) Input/Output constraints on the variable Forces the compiler to materialize values, forget compile-time constants (defeating constant propagation), and execute the code.

1. The "memory" Clobber acts as an Opaque Function Call

The inline assembly in DoNotOptimize() contains a "memory" clobber.

  • Opaque External Call Simulation: To the compiler's optimizer, a "memory" clobber is treated just like an external, non-inline function call (whose source code is hidden). The compiler must assume this block can read or write any globally-reachable object.
  • Preventing Reordering with time(): Since time() itself is not defined inline in C++ headers, the compiler cannot see its definition and treats it as an opaque external function. Because both time() and DoNotOptimize() (due to its "memory" clobber) are treated as opaque operations that could modify global state, the compiler is strictly forbidden from reordering them. It is the exact same reason a compiler will not reorder two sequential calls to unknown functions like puts("first"); puts("second");.

2. The "+r,m" Constraint & Data Dependency

While the "memory" clobber handles ordering against other opaque calls, the actual benchmark computation (e.g., run_bench()) is ordered using strict C++ data dependencies.

  • Forcing Materialization: The "+r,m" constraint declares a read/write operand. This forces the compiler to materialize the target variable's value in a register or memory, meaning the variable must actually be computed up to that point.
  • Defeating Constant Propagation: Because of the + (read/write) modifier, the compiler must assume that the assembly block could arbitrarily change the variable's value. Consequently, the compiler has to forget all compile-time knowledge of that variable (such as it being a constant, positive, etc.). This prevents the optimizer from constant-folding or optimizing away the benchmarked code entirely.
  • Enforcing the Pipeline:
    1. DoNotOptimize(input) forces the input to be computed and clears compiler assumptions.
    2. The compiler runs the benchmark calculation, because the output depends on this newly materialized input.
    3. DoNotOptimize(output) forces the output of the benchmark to be materialized in memory or a register, ensuring the compiler doesn't optimize away the entire benchmark calculation as an "unused result".

3. Key Misconceptions Corrected by the Answer

  • Reference Arguments and the Stack: The original poster suspected that reference arguments force variables onto the stack (since you cannot have a pointer to a register). However, because DoNotOptimize() is forced to inline (always_inline), the by-reference argument is optimized away entirely. The assembly operates directly on the underlying C++ variable.
  • Register vs. Memory Choice: The compiler can always choose to load a variable's value into a register before the asm statement and store it back afterward, even with the "m" constraint option.
  • Clang Workaround: The use of "r,m" (instead of "rm") specifically bypasses a Clang missed-optimization bug where Clang aggressively defaults to memory spilling for "rm" constraints even when a register is readily available.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.