# How do we audit a low-level C or C++ loop for undefined behavior before enabling aggressive optimization?

> Audit C and C++ loops for overflow, bounds, lifetime and aliasing errors before optimization, using explicit contracts and targeted sanitizer checks.

Canonical URL: https://www.devobs.io/articles/qa-ge50-audit-low-level-loops-undefined-behavior/
By: Lena Fischer
Published: 2025-12-07T20:27:54.895Z
Updated: 2026-09-06T10:18:15.722Z
Section: Architecture

Before enabling `-O2`, `-O3`, or `-Ofast`, audit the loop by fixing its source-level contract first, then proving the loop stays within that contract on every iteration. Write down valid inputs, object lifetime, aliasing assumptions, and arithmetic bounds before looking at generated code. Tests, UBSan, and assembly inspection are useful bug-finders, but they do not prove the absence of undefined behavior over all inputs. GCC explicitly adds stricter aliasing and stronger loop transforms at higher optimization levels in its [Optimize Options documentation](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html).

## What boundary are you auditing?

Set the boundary in enforced order: language mode, compiler and exact flags, target architecture, and the function’s valid-input contract. Then state which pointers refer to which live objects, how long those objects live, and whether overlapping access is allowed.

That ordering matters because optimization is defined against the source language rules, not against a test run. If your contract is vague, the loop is not ready for aggressive optimization review.

## Which assumptions in the loop need a proof?

For a low-level loop, prove these specific points:

- No signed overflow in induction variables, index scaling, address calculations, or value updates.
- Every dereference addresses a valid element of the same live array object. A one-past pointer may be formed but never dereferenced.
- No reads from indeterminate storage.
- Shift counts are in range, and signed shifts satisfy the rules of the selected C or C++ language version.
- Accesses respect aliasing rules. GCC documents that `-O2` enables `-fstrict-aliasing`, and `-O3` adds further loop optimizations in the same [Optimize Options documentation](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html).

Using an unsigned index can remove signed-overflow UB for that variable, but it does not prove correctness. A wrapped unsigned value can still produce a wrong bound or pointer.

## What does a worked loop audit look like?

Consider:

```c
#include <stddef.h>

// C99 or later: the for initializer declares i.
// dst addresses n writable, initialized int elements in one live array.
void add_bias(int *dst, size_t n, int bias) {
    for (size_t i = 0; i < n; ++i) {
        dst[i] += bias;
    }
}
```

The declaration `for (size_t i = 0; ...)` requires C99 or later in C; this is not a C90 example. Assume no conflicting concurrent access. The loop also requires:

1. `dst` is non-null when `n > 0`.
2. `dst + i` stays within the same array object for every `0 <= i < n`.
3. Each `dst[i]` names a live, initialized `int`.
4. `dst[i] + bias` is representable as `int` on every iteration.

Point 4 is the common optimization trap. Pointer bounds may be perfect while the addition still has UB.

A useful review checklist is: contract written, trip count bounded, arithmetic ranges justified, pointer range justified, lifetime documented, aliasing documented, and edge inputs tested.

## Which tools help, and what do they not prove?

Use compiler warnings, static analysis, and runtime sanitizers to look for violations of the contract. Clang’s UBSan instruments the compiled program to detect undefined behavior on executed paths. It lists checks including signed overflow, null, misalignment, shifts, and some bounds cases in the [UndefinedBehaviorSanitizer documentation](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html). That is strong evidence for executed paths, not a proof for untested ones.

Use LLVM IR or assembly only when you need to explain a surprising transformation. LLVM describes its IR as a common code representation throughout compilation, supporting transformations and analysis in the [LLVM Language Reference](https://llvm.org/docs/LangRef.html). That makes it a good debugging view, not an oracle that the original C or C++ was valid.

## Questions about sanitizer and optimization limits

**Does a clean UBSan run mean the loop is safe?**  
No. It means enabled checks did not fire on the paths you executed.

**Should I enable `-Ofast` once `-O3` looks fine?**  
Only if you also accept relaxed standards assumptions. GCC states that `-Ofast` enables optimizations “not valid for all standard-compliant programs” in its [Optimize Options documentation](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html).

Your next step is to take one performance-critical loop, write its valid-input contract as review text next to the function, and reject optimization changes until each arithmetic and memory assumption is justified against that contract.

Reviewed: 2026-09-06.

## Source references

- <https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html>
- <https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html>
- <https://llvm.org/docs/LangRef.html>
