Gries’ discipline does not stop at a correct program. Once a loop is proven, you can make it faster by the same calculus that made it correct — not by abandoning the invariant, but by strengthening it so an expensive quantity is carried along and maintained incrementally instead of recomputed from scratch.
Student edition · understand, apply, verify
Optimize through maintained relations without changing numeric or observable semantics accidentally.
What you should be able to do: Strengthen invariants for cached computations and state the cost model.
Assumes basic programming, not formal-methods notation. This guide defines the needed notation locally. Scalar proofs use mathematical integers unless stated otherwise; finite arrays require integer valid bounds. Guarded-command notation is not Python syntax. Machine overflow, floats, exceptions, aliasing, and concurrency need additional contracts.
This page is one chapter of a series. It assumes the loop machinery from Developing programs & inventing invariants and the derivations in Worked examples. Read those first if the words invariant and bound function are not yet reflexes.
There is a folk belief that making a program fast means bending or breaking its clean structure — that correctness and speed pull in opposite directions. Gries shows the reverse. The most reliable optimisation of a loop is a program transformation that adds a new variable and a new conjunct to the invariant, so that a term the loop was recomputing on every pass is instead updated in a step or two. The invariant gets stronger, the program gets faster, and the correctness argument gets richer rather than weaker. This is the same move behind “strength reduction” and “loop-invariant code motion” in compilers — here derived by hand, with proof.
This is an original study summary for quick reference. Notation follows Gries and Dijkstra: {P} S {Q} for a Hoare triple, guarded commands (do…od) for loops, P for the loop invariant, t for the bound function. See the book for the full, rigorous treatment.
Read these definitions first, or use the linked terms as you go. The examples and limitations are local to this guide; companion pages are optional.
Refinement and observational equivalence
Replace an implementation while preserving the required observable contract, including outputs/effects. Optimize by adding maintained relations, not discarding correctness.
Example. A running sum adds invariant s=sum(prefix) instead of recomputing that prefix each time.
Boundary. Floating-point reassociation, exceptions, mutation, and concurrency can make apparent mathematical equivalence fail at runtime.
Loop invariants
A predicate true after initialization and after each iteration. Together with the false exit guard it establishes the required result.
Example. While summing, preserve 0≤i≤n and s=sum(b[0:i]); add b[i] then advance i.
Boundary. An invariant alone does not prove termination. Validate array/domain bounds and establish the invariant before iteration.
Complexity and workload assumptions
Big-O describes growth with input size under a cost model. Worst-case, expected, amortized, and output-sensitive bounds are distinct promises.
Example. Binary search is O(log n) comparisons on sorted random-access input; printing every subset needs exponential output work.
Boundary. Expected hash-table O(1) is not worst-case O(1). Include output size, recursion space, input assumptions, integer costs, and library operations.
Preconditions and postconditions
A precondition states required inputs/state before an operation. A postcondition states the result promised when those requirements hold and execution terminates normally. Together they make requirements testable.
Example. For sum_first(b,n), require integer 0≤n≤len(b) and numeric elements; promise the sum of b[0:n].
Boundary. State exceptions, mutation, numeric domains, and empty cases. A mathematical array range does not automatically validate a runtime input.
Correctness first, then efficiency
The order of operations is not negotiable. First derive a correct loop from its specification, invariant, and bound — exactly the discipline of the earlier chapters. Then, and only then, look at it as an object to be improved. Efficiency is a second pass over a program you already trust.
The crucial claim is about how you improve it. You do not weaken the invariant to give the compiler or yourself more room; a weaker invariant proves less, and a loop that proves less is a loop you no longer trust. Instead you strengthen the invariant by adding a conjunct that records some quantity, and then you have a new obligation — to keep that conjunct true — whose cheapest discharge is an incremental update. The speed comes precisely from the extra thing you now guarantee.
Efficiency comes from STRENGTHENING the invariant — never from abandoning it. A faster loop proves more, not less.
Read it as a transformation with a contract: the transformed loop must compute the same result as the original (same postcondition), so every step is a semantics-preserving rewrite justified by the invariant, not a hopeful edit. Because it is provably equivalent, you get the speed without reopening the question of correctness.
Taking an expression out of a loop
Here is the core move in its plainest form. Suppose the loop body evaluates some expression E on every iteration, where E depends on the loop’s variables, and computing E from scratch is expensive — a sum over a prefix, a power, a running product, a scan. If E changes only a little from one iteration to the next, you are paying full price for a small delta.
The fix: introduce a fresh variable e, and add the conjunct e = E to the invariant. Now e always holds the current value of E. Establish it once in the initialisation, maintain it with a cheap update in the body wherever the loop changes the variables E depends on, and replace every use of E by e.
Body recomputes expensive E(vars) each pass? 1. add invariant conjunct e = E 2. establish e = E in the init 3. update e incrementally in the body 4. replace uses of E by e Now E is computed once, then maintained — not recomputed.
The obligation the new conjunct creates is exactly what tells you the update. Whatever the body does to the underlying variables, you must restore e = E before the iteration ends. Solving that little equation — “what must I add to e so that it again equals the new E?” — is the whole trick, and it is pure algebra.
Worked example: running sum
Compute s = Σ b[0..n-1], the sum of the first n elements of an array. Derive it first, then observe the waste, then transform.
# Specification# pre Q: n ≥ 0# post R: s = (sum j : 0 ≤ j < n : b[j])
Replace the constant n by a variable i to get the loop invariant — the answer computed so far, over the prefix b[0..i-1]:
P: 0 ≤ i ≤ n ∧ s = (sum j : 0 ≤ j < i : b[j])
The naive way to write the body is to take the invariant literally and, after bumping i, recompute the whole prefix sum. That re-adds b[0], b[1], … from the start on every pass — the loop is O(n) iterations each doing O(i) work, so O(n²) overall. But the strengthened form already holds the sum in s; the only change from one iteration to the next is one new term b[i]. So s := s + b[i] restores the invariant in O(1).
✗ Naive — recompute the sum each pass
i := 0 ; s := 0 ;
do i ≠ n →
i := i + 1 ;
# re-derive s from scratch over b[0..i-1]
s := 0 ;
k := 0 ;
do k ≠ i →
s := s + b[k] ; k := k + 1
odod# E = (sum j:0≤j<i:b[j]) recomputed every pass# total work: O(n²)
✓ Strengthened — maintain s incrementally
i := 0 ; s := 0 ;
# invariant P: 0 ≤ i ≤ n and# s = (sum j:0≤j<i:b[j])# bound t: n - ido i ≠ n →
s := s + b[i] ; # restore s = sum over 0..i
i := i + 1 # now s = sum over 0..(new i)-1od# {s = (sum j:0≤j<n:b[j])}# total work: O(n)
The strengthened conjunct s = Σ b[0..i-1] was already in the invariant — that is the point. The naive body discards it and rebuilds; the good body uses it, adding only the delta b[i]. Note the update order: add b[i] while i still points at the new element, then advance i, so that s = Σ b[0..i-1] holds again at the bottom of the loop.
Worked example: strength reduction on squares
Print (or store) i*i for i = 0, 1, …, n using no multiplication at all — only addition. On machines where a multiply is far dearer than an add, this is a real win, and it is the textbook case of strength reduction: replacing an expensive operation with a cheaper one maintained incrementally.
# Goal: at each step have sq = i*i, for i = 0..n, using only +# Expensive expression E = i*i
Add the conjunct sq = i*i to the invariant. The body increments i, so we must find the cheap update that restores sq = i*i for the new i. The algebra:
(i+1)² = i² + 2i + 1
# so when i becomes i+1, sq must grow by (2i + 1)
But 2i + 1 still contains a multiplication. Apply the technique a second time: introduce another variable d with the conjunct d = 2i + 1, the amount by which sq must increase. When i becomes i+1, the increment itself changes: 2(i+1) + 1 = (2i + 1) + 2, so d grows by the constant 2. Now nothing multiplies.
P: 0 ≤ i ≤ n ∧ sq = i*i ∧ d = 2*i + 1
✗ Naive — a multiply every pass
i := 0 ;
do i ≠ n + 1 →
use(i * i) ; # E = i*i recomputed
i := i + 1
od# one multiplication per iteration
✓ Strengthened — additions only
i := 0 ; sq := 0 ; d := 1 ;
# invariant P: sq = i*i and d = 2*i + 1# bound t: (n + 1) - ido i ≠ n + 1 →
use(sq) ; # sq = i*i, no multiply
sq := sq + d ; # (i+1)² = i² + (2i+1)
d := d + 2 ; # 2(i+1)+1 = (2i+1) + 2
i := i + 1
od# init check: i=0 ⇒ sq=0=0², d=1=2·0+1 ✓
Each derivation step was forced. The update sq := sq + d came from expanding (i+1)²; the update d := d + 2 came from expanding 2(i+1)+1; the initial values sq = 0, d = 1 came from substituting i = 0 into the two new conjuncts. Nothing was guessed. This double application is characteristic: reducing one expression often exposes another, and you strengthen again until only constants remain.
Worked example: maintaining a power
Suppose a loop needs x^i at each step for i = 0, 1, …, n. Recomputing pow(x, i) from scratch costs O(i) multiplications per pass (or O(log i) with fast exponentiation) — wasteful when consecutive powers differ by a single factor of x.
Add the conjunct p = x^i to the invariant. Since x^(i+1) = x^i · x, the update that restores it when i advances is a single multiplication, p := p*x. The base case x^0 = 1 gives the initialisation.
P: 0 ≤ i ≤ n ∧ p = x^i
✗ Naive — recompute the power
i := 0 ;
do i ≠ n + 1 →
use(pow(x, i)) ; # E = x^i from scratch
i := i + 1
od# O(i) (or O(log i)) mults every pass
✓ Strengthened — one multiply per pass
i := 0 ; p := 1 ; # x^0 = 1# invariant P: p = x^i# bound t: (n + 1) - ido i ≠ n + 1 →
use(p) ; # p = x^i
p := p * x ; # x^(i+1) = x^i · x
i := i + 1
od
This is the same idea that underlies fast exponentiation, where an invariant of the form result * base^exp = x^n is maintained while exp is halved — each step preserves a product-shaped invariant with a cheap update. See the exponentiation derivation in the worked examples: the discipline is identical, only the invariant’s shape differs. Whenever a quantity you need next iteration is a simple function of the one you have now, an invariant conjunct turns recomputation into a single step.
The general recipe
Every example above is the same five moves. Here they are as a checklist you can run on any proven loop that recomputes something.
Spot a recomputed expression E. Look in the body for a term, written in the loop’s variables, that is evaluated every pass and is expensive relative to the rest of the iteration — a sum, a product, a power, a scan, a repeated index computation.
Add the conjunct e = E to the invariant. Introduce a fresh variable e whose sole job is to always equal E. This strengthens the invariant — you now promise more.
Fix the initialisation to establish it. Set e to the value of E in the loop’s starting state (substitute the initial values of the variables into E), so e = E holds before the first iteration.
Derive the incremental update. Given how the body changes the variables E depends on, solve for the cheap adjustment to e that re-establishes e = E at the bottom of the loop. This algebra is the heart of the transformation; if the adjustment still contains an expensive term, apply the recipe again to it (as with the squares example).
Replace uses of E by e. Every place the body read E, read e instead. The expensive computation is now gone from the body, replaced by a value that was maintained cheaply.
spot E → add conjunct e = E → init e → derive cheap update → replace E by e. The invariant gets stronger; the body gets faster.
Note that steps 2 through 5 mirror exactly the four loop obligations from the correctness checklist — establish the invariant, preserve it, and use it — applied to the new conjunct. That is why the transformation cannot break correctness: it discharges its own proof obligation by construction.
Trade-offs
The transformation is nearly free in correctness terms but not free in every other sense. Weigh it honestly.
Dimension
Cost of strengthening
Benefit
Extra state
One (or more) new variables live for the loop’s duration.
The expensive expression is now available in O(1).
Extra code
Initialisation line plus an update line per new variable.
Removes a full recomputation — often O(i) or O(log i) — from every pass.
Asymptotic cost
None; updates are constant-time per pass.
Turns O(n²) into O(n), or a multiply into an add.
Readability
The body no longer states what it computes; the meaning now lives in the invariant comment.
Preserved — if you write the invariant conjunct as a comment, intent is documented precisely.
Proof burden
One more conjunct to establish and preserve.
The proof is a provably-correct transformation, not a risky hand edit.
When not to bother. If E is already cheap (a couple of additions), the extra variable and update line buy nothing and cost clarity. If the loop is cold — run rarely, or over tiny inputs — the constant-factor win is invisible and the readability tax is real. And if the incremental update turns out to be as expensive as recomputing E, there is no delta to exploit; the technique only pays when E changes little between iterations.
Measure before optimizing — profile to confirm the loop and the expression actually matter before you touch anything. But once you have decided to act, prefer this technique: unlike most speed hacks, strengthening the invariant is a provably-correct transformation. The faster program computes exactly what the original did, and you can see why line by line.
Worked example: maintain a running prefix sum
Work through the state changes and the exact claim being checked.
Sequence for this example. The table explains the evidence and decision at each step.
Step
Evidence, decision, and expected result
Start with a correct slow computation
Recompute the whole prefix on each step; correctness can hold but total work is quadratic.
Add cached state
Maintain s=sum(prefix), initialized to zero.
Update incrementally
Advance by adding the new element, reducing prefix recomputation to constant work per step.
Check the numeric model
Integer arithmetic is exact in the mathematical/Python integer model. Floating-point regrouping can change rounding and requires a separate accuracy contract.
Check your understanding: Can an algebraic rearrangement change float results?
Yes. Real-number equivalence does not guarantee identical finite-precision evaluation.
Run and change the example
Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:
python3 guide_labs.py optimization
python3 guide_labs.py test
The complete standard-library lab checks the stated finite cases and boundary inputs. Its invariants explain the reasoning; tests alone do not prove all possible inputs.
The program tests its stated finite contracts. It is an educational model, not an implementation of the named production product or a proof of unmodeled distributed faults.
Derive a correct loop, then transform it — efficiency is a second pass over a program you already trust.
Strengthen, never abandon
Speed comes from a stronger invariant; a faster loop proves more, not less.
Take E out of the loop
Add conjunct e = E, initialise it, update it incrementally, replace uses of E by e.
Running sum
Carry s = Σ b[0..i-1]; body does s := s + b[i] — O(n) not O(n²).
Strength reduction on squares
(i+1)² = i² + 2i + 1; maintain sq = i*i and d = 2i+1 with additions only.
Maintaining a power
x^(i+1) = x^i · x; keep p = x^i, update with a single p := p*x.
The recipe
Spot E → add e = E → init → derive cheap update → replace — and repeat if the update is still costly.
Trade-offs
Extra state and update code, some readability cost; skip when E is cheap or the loop is cold.
The recurring theme: the invariant is still the design — even for speed. Making a loop faster is not a departure from proof-driven programming; it is one more application of it. You strengthen what stays true across every iteration, and the cheaper program falls out, correct by construction.