🧬 Types & Qualifiers, In Depth
restrict: the no-aliasing promise
▶ Open the interactive lesson — free, no signupThe C library ships two functions — memcpy and memmove — that seem to do exactly the same job. The entire difference is one promise about overlapping data: a promise that lets the compiler copy dramatically faster, and silently breaks your program if you ever make it falsely. This lesson shows you when to make that promise in your own code — and when you absolutely must not.
Two pointers alias when they refer to the same memory. Aliasing is legal, common — and quietly devastating for performance, because the compiler must assume it's happening everywhere. restrict, added in C99 — the 1999 edition of the C standard — is your way of saying: "through this pointer, and this pointer alone, will this data be accessed — optimize accordingly."
Seeing the problem
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
#include <stdio.h>
void add_twice(int *val, int *inc) {
*val += *inc;
*val += *inc;
}
int main(void) {
int a = 3, b = 10;
add_twice(&a, &b); /* distinct objects */
printf("distinct: %d\n", a);
int x = 3;
add_twice(&x, &x); /* aliased! val == inc */
printf("aliased : %d\n", x);
return 0;
}🤔 What are the two printed values?
Think first
distinct: 23 (3+10+10) but aliased : 12 — the first *val += *inc doubles x to 6, and the second one reads the updated value: 6+6 = 12. Same source line, different meaning, purely because of aliasing. This is why the compiler cannot rewrite the body into the "obvious" *val += 2 * *inc;.
Because that aliased outcome is possible, the compiler must compile the paranoid version: load *inc, add, store, load *inc again (it might have changed!), add, store. Every write through one pointer forces re-reads through others. In a loop over big arrays, this poisons everything — especially vectorization, which needs to process many elements in flight at once.
🧠 Checkpoint: Why must the compiler re-load *inc after the store to *val (without restrict)?
- int loads cannot be cached in registers
- val and inc might point to the same object, so the store may have changed *inc
- Function parameters are always volatile
- It does not — compilers always keep it in a register
Show answer
val and inc might point to the same object, so the store may have changed *inc — The mere possibility of aliasing forces the reload. The compiler must generate code that is correct for EVERY legal call, including add_twice(&x, &x).
The fix: a promise, and the payoff
void add_twice(int *restrict val, int *restrict inc) {
*val += *inc; /* compiler may now assume val != inc */
*val += *inc;
}
/* gcc -O2, x86-64:
without restrict with restrict
mov eax, [rsi] mov eax, [rsi]
add [rdi], eax add eax, eax ; 2 * *inc
mov eax, [rsi] ; reload mov [rdi]... one store, no reload
add [rdi], eax */With restrict, the second *inc load disappears — the compiler keeps it in a register. In real loops the win is far bigger: proving arrays don't overlap is exactly what lets the compiler unleash SIMD instructions and process 4–8 elements per cycle.
The standard library's own example: memcpy vs memmove
You've been using restrict all along — look at these real prototypes from <string.h>:
void *memcpy(void *restrict dst,
const void *restrict src, size_t n);
/* YOU promise: no overlap */
void *memmove(void *dst, const void *src, size_t n);
/* overlap allowed, handled */$ cat overlap.c # shifting "abcdef" right by two, in place: # memmove(s + 2, s, 4) -> safe, prints "ababcd" # memcpy (s + 2, s, 4) -> UB: breaks memcpy's restrict contract $ gcc -O2 overlap.c && ./a.out ababcd # the memcpy version may "work", corrupt data, or change with any upgrade
memcpy declares both pointers restrict: you promise no overlap, so it may copy with the fastest possible wide, reordered loads and stores. memmove makes no such promise — it must check the direction and copy carefully, slightly slower but overlap-safe. Same job, different contract: that's restrict in a nutshell.
🧠 Checkpoint: You need to copy a region two bytes to the right within the same buffer. Which call is correct?
memcpy(buf+2, buf, n)— it is fastermemmove(buf+2, buf, n)— its contract permits overlap- Either; they are interchangeable
- Neither; overlapping copies are impossible in C
Show answer
memmove(buf+2, buf, n) — its contract permits overlap — The regions overlap, and memcpy's restrict-qualified parameters make overlap UB. memmove exists precisely for this: it detects direction and copies safely. When in doubt, memmove — correctness first.
It's a promise YOU make — and UB if you break it
restrict is completely unchecked. The compiler cannot verify your pointers don't overlap — it simply believes you and optimizes on that basis. Call memcpy with overlapping buffers, or pass the same array as both src and dst of a restrict-qualified function, and you get undefined behavior: code that "worked for years" breaks on the next compiler upgrade, and the compiler is blameless. If overlap is even possible, don't write restrict.
Precisely stated, the promise is: during the lifetime of a restrict pointer, if the object it points to is modified, then all access to that object happens through that pointer (or expressions derived from it, like p + i). Read-only sharing is fine — modification is what triggers the exclusivity clause.
🧠 Checkpoint: What happens if you break a restrict promise?
- The compiler detects it and warns
- A runtime exception
- Nothing — restrict is only documentation
- Undefined behavior: the optimizer generated code assuming no aliasing
Show answer
Undefined behavior: the optimizer generated code assuming no aliasing — restrict is an unchecked contract. The compiler bakes the no-aliasing assumption into the generated instructions; if reality disagrees, those instructions compute garbage. No diagnostic is required, and none typically comes.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
restrict is one honest conversation between you and the optimizer; next we tackle the whole system of type conversations C holds behind your back — implicit conversions, promotions, and casts.
▶ Practice this lesson interactively (with live gcc)