5.1.32 · D2C Programming

Visual walkthrough — restrict keyword — aliasing hint

2,661 words12 min readBack to topic

We will walk this loop the whole way:

for (int i = 0; i < n; i++)
    a[i] = b[i] + c[i];

Everything below is about what the compiler is allowed to do with these three pointers.


Step 1 — What a pointer actually is (a house address)

WHAT. A pointer is just a number: the address of a box in memory. a, b, c are each an address of the first box of a row of boxes; a[i] means "the box i steps after where a starts."

WHY start here. Every later argument is about whether two of these addresses can land on the same box. If you don't picture memory as a numbered street of boxes, "overlap" is a meaningless word.

PICTURE. Below, memory is a row of numbered cells. b and c point at two separate rows; a points at a third. The label under each arrow is the address it holds.

Figure — restrict keyword — aliasing hint

Step 2 — The compiler's fear: maybe the rows secretly touch

WHAT. Nothing in the function signature void add(int *a, int *b, int *c, int n) forbids the rows from overlapping. So the compiler must imagine the worst case: perhaps a is b shifted by one box.

WHY. A compiler is only allowed to change your code if the change is guaranteed to give the same answer for every legal input. If overlap is legal, it must survive overlap.

PICTURE. Here is the poisonous case the compiler dreads: a = b + 1. Now a[i] and b[i+1] are literally the same box (yellow). Writing a[i] silently rewrites the value b[i+1] that a later iteration will read.

Figure — restrict keyword — aliasing hint

Step 3 — Why fear forces a strict order (WHAT each memory op does)

WHAT. Because a store into a[i] might change some future b[?] or c[?], the compiler must run the three memory operations in exactly source order, every single iteration:

  • — fetch the summand from row b.
  • — fetch the summand from row c.
  • — write the sum; this arrow is the threat — it might land on a box a future load needs.

WHY. If the compiler reordered — say loaded b[i+1] early into a register — but the store to a[i] overwrote that very box, the register would hold a stale value. So reordering is banned; caching across iterations is banned.

PICTURE. The three ops chained by hard dependency edges. The red edge is the store→future-load hazard that pins everything down.

Figure — restrict keyword — aliasing hint

Step 4 — The promise: restrict erases the shared box

WHAT. Now we add the qualifier:

void add(int * restrict a, const int * restrict b,
         const int * restrict c, int n);

WHY this tool and not const? const promises "I won't write through this pointer." That's a different promise — it says nothing about two pointers touching. We need a promise about addresses being disjoint, and restrict is the only C99 tool that says exactly that:

Each is you swearing no box is shared.

PICTURE. The same three rows, but the yellow shared box is gone — a bright wall separates them. The store arrow can no longer reach another row's boxes.

Figure — restrict keyword — aliasing hint

Step 5 — With no shared box, order collapses

WHAT. Since a store to a[i] can never hit a b or c box, the store→load hazard from Step 3 disappears. The dependency chain breaks into three independent streams.

WHY. Independence is the currency of optimization. Independent operations can be reordered, batched, and cached freely, because none can spoil another.

PICTURE. The chained diagram of Step 3, now with the red hazard edge cut. The three ops float free.

Figure — restrict keyword — aliasing hint

Step 6 — Freedom becomes SIMD (many boxes at once)

WHAT. Free of ordering, the compiler grabs a block of b and a block of c, adds them in one wide instruction, and stores a block of a. This is vectorization / SIMD — one instruction operating on several elements.

WHY. A CPU can add, say, 8 integers in the time it took to add 1. This is only legal because Step 5 proved the boxes are independent — no store can invalidate a value already loaded into a wide register.

PICTURE. Four boxes of b and four of c loaded together, summed by one wide "+", stored as one block into a.

Figure — restrict keyword — aliasing hint
Figure — restrict keyword — aliasing hint

Step 7 — The degenerate case: what if you LIE

WHAT. First, the function we are calling — note the parameter order, destination comes first, exactly like memcpy(dst, src, n):

// copy n ints from src into dst; PROMISES dst and src regions never overlap
void copy(int * restrict dst, const int * restrict src, int n) {
    for (int i = 0; i < n; i++) dst[i] = src[i];
}

So in copy(dst, src, n) the first argument is where we write, the second is where we read. Now we lie about the wall:

int arr[4] = {1,2,3,4};
copy(arr + 1, arr, 3);   // dst = arr+1 (write here), src = arr (read here) -> they OVERLAP

WHY show this. Every promise has a failure mode. Here the compiler believed Step 4's wall existed and did Step 6's batching — but the wall was a lie. It reads stale/fresh boxes in whatever order it chose. The result is undefined behaviour: maybe {1,1,2,3} (a byte-forward dst[i]=src[i] that keeps re-reading boxes it just wrote), maybe {1,1,1,1} (block/backward orders), maybe {1,2,3,4}-flavoured garbage. Two different answers from the same call ⇒ undefined.

PICTURE. The overlap of arr and arr+1, with two possible outputs branching from the same source — proof the answer isn't determined.

Figure — restrict keyword — aliasing hint

Step 8 — Edge case: the single scalar behind a pointer

WHAT. A subtler win — one pointer that isn't even an array:

void scale(float * restrict v, float * restrict factor, int n) {
    for (int i = 0; i < n; i++) v[i] *= *factor;
}

WHY. Without restrict, factor might point inside v. Then writing v[i] could change *factor, so the compiler must re-read *factor every iteration. With restrict factor, it proves factor is outside v and loads *factor once into a register.

PICTURE. Left: factor feared to live inside v, forcing a reload arrow each step. Right: factor walled off, loaded once (single arrow to a register).

Figure — restrict keyword — aliasing hint

The one-picture summary

The whole chain — fear → forced order → promise → freedom → speed — in one frame.

Figure — restrict keyword — aliasing hint
Recall Feynman: retell the whole walkthrough in plain words

Memory is a long street of numbered boxes, and a, b, c are arrows pointing at three rows of them. The compiler is terrified that two rows secretly share a box (Step 2), because if writing to a could scribble on b, it must do everything slowly and in order, re-reading after every write (Step 3). C's built-in type rule only separates rows of different types; for our same-type int* rows it says nothing, so you calm the compiler with restrict: "I swear these rows never touch" (Step 4) — a promise that only covers the pointers descended from a, b, c, not some wild third pointer or arithmetic that runs off the end. Now no write can spoil a future read, so the operations float free (Step 5), and the CPU can add a whole block of numbers in one shot (Step 6) — with a tiny scalar cleanup for the last n mod W leftovers. But if you lie — hand it overlapping rows while swearing they're separate — it batches anyway and produces garbage nobody can predict (Step 7). The same trick even lets a lone *factor be read once instead of every loop (Step 8). One promise about addresses buys you all the speed.


Active recall

Recall Check your picture
  • In Step 2, which single fact makes the compiler pessimistic? (one shared box is possible)
  • How does restrict differ from C's built-in strict (type-based) aliasing? (type rule separates different types for free; restrict supplies the promise for same-type pointers)
  • In Step 3, which arrow is the hazard that pins the order? (store a[i] → future load)
  • Why can't const do restrict's job? (const = won't-write; restrict = won't-overlap)
  • Whom does the restrict promise actually cover? (the pointer and pointers derived from it — not unrelated pointers)
  • In Step 6, what is the "tail" and how is it handled? (n mod W leftover elements; a scalar cleanup loop)
  • In copy(dst, src, n), which argument is written to? (the first, dst)
  • In Step 7, what makes the two outputs both "correct"? (overlap + lie ⇒ undefined behaviour)
What single possibility makes the compiler refuse to reorder the loop?
That two rows might share one box (aliasing) — one overlapping address is enough.
How does restrict differ from C's built-in strict (type-based) aliasing?
Strict aliasing lets the compiler assume different-type pointers don't overlap for free; restrict supplies that promise for same-type pointers, where the type rule is silent.
Which memory operation is the reordering hazard in the add loop?
The store to a[i], because it might overwrite a box a later load needs.
Why is restrict the right tool and not const here?
const promises no writes through the pointer; restrict promises the pointer's boxes don't overlap another's — only the second unlocks reordering.
Whom exactly does the restrict promise cover?
The restrict-qualified pointer and pointers derived from it; unrelated pointers may still alias, and out-of-bounds arithmetic is itself undefined behaviour.
After the restrict promise, why can the loop vectorize?
With no shared box, no store can invalidate a loaded value, so operations are independent and can run in wide blocks.
In copy(dst, src, n), which parameter is the destination?
The first one, dst (write target); src is the second (read source), matching memcpy order.
What is the SIMD "tail" and how is it processed?
The n mod W leftover elements when n isn't a multiple of the lane width W; the compiler emits a scalar cleanup loop (or masked op) for them.
If restrict-marked pointers actually overlap, what is the result?
Undefined behaviour — the output isn't determined, often only wrong at -O2/-O3.
In v[i] *= *factor; why mark factor restrict?
It proves factor isn't inside v, so *factor is loaded once into a register instead of re-read each iteration.

Connections

  • Yeh note Hinglish mein →
  • Pointers in C
  • Pointer aliasing
  • const qualifier
  • memcpy vs memmove
  • Compiler optimization & vectorization (SIMD)
  • Undefined behaviour in C
  • C99 standard features

Derivation Map

worst case

blocked until promise

no shared box

if false

Memory is numbered boxes

Rows might share a box

Forced strict order

restrict promise walls rows

Ops become independent

Block add SIMD plus tail

Lie causes UB

Load scalar once