Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 11 – Reading compiler output

Optimisation and undefined behaviour in assembly

What -O2 does to C, read in AArch64, x86-64 and RISC-V listings: inlining, tail calls, unrolled and vectorised loops, and checks undefined behaviour deletes.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: An optimised listing shows four fingerprints · Undefined behaviour lets the compiler delete a check · Practise. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Identify inlining, a tail call, an unrolled loop and a vectorised loop in an -O2 listing
  • Explain why an overflow test written as x + n < x compiles to a single shift at -O2
  • Read the main loop, the reduction and the leftover loops of a vectorised loop on three ISAs
  • Detect and fix checks that undefined behaviour lets an optimising compiler delete
  • Choose between restrict, a loop pragma, intrinsics and hand-written assembly for a hot loop

Before you start

What you’ll learn · Before you start

What you will learn

  • Identify inlining, a tail call, an unrolled loop and a vectorised loop in an -O2 listing
  • Explain why an overflow test written as x + n < x compiles to a single shift at -O2
  • Read the main loop, the reduction and the leftover loops of a vectorised loop on three ISAs
  • Detect and fix checks that undefined behaviour lets an optimising compiler delete
  • Choose between restrict, a loop pragma, intrinsics and hand-written assembly for a hot loop

Before you start

On this page

At -O0, Clang compiles sum_squares in opt.c with two calls of a helper; at -O2 the calls are gone, and a multiply and a multiply-add remain. The same switch turns x + n < x, a test for overflow, into one shift that never reads x, and for x = INT_MAX - 50 and n = 100 the -O2 build answers 0 where the -O0 build answered 1. Reading optimisation and undefined behaviour in assembly means asking what the compiler was allowed to assume.

Optimisation shows as code that is missing, moved or repeated; undefined behaviour, as a test that is not there.

An optimised listing shows four fingerprints

At -O0 each function of opt.c keeps the shape of its source; at -O2 each one changed in a way you can learn to spot:

opt.c C · opt.c
// opt.c: three small functions whose -O2 listings show what the optimiser did to them

// square is static, so no other file can call it: -O2 is free to copy its body into the caller.
static int square(int x) { return x * x; }
int sum_squares(int a, int b) { return square(a) + square(b); }

// scale is defined elsewhere; calling it is the last thing forward does.
int scale(int x);
int forward(int x) { return scale(x + 1); }

// A loop whose trip count, 3, is known while compiling.
int dot3(const int *a, const int *b) {
    int s = 0;
    for (int i = 0; i < 3; i++)
        s += a[i] * b[i];
    return s;
}
opt.c at -O0 and -O2, and the vector registers in sum at -O2 Python · fingerprints_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# opt.c at -O0 for AArch64 Linux: the labels and every call (bl) in the listing
clang --target=aarch64-linux-gnu -O0 -S -fno-asynchronous-unwind-tables -o opt_O0.s opt.c
grep -E '^[a-z_0-9]+:|[[:space:]]bl[[:space:]]' opt_O0.s
# The same file at -O2, with directives and comments filtered out
clang --target=aarch64-linux-gnu -O2 -S -fno-asynchronous-unwind-tables -o opt_O2.s opt.c
grep -v -E '^[[:space:]]+[.]|^[.]L|^[[:space:]]*//' opt_O2.s | sed -E 's/[[:space:]]+\\/\\/.*$//'
# sum.c at -O2: the lines of its listing that use vector registers (q0 to q31, v0 to v31)
clang --target=aarch64-linux-gnu -O2 -S -fno-asynchronous-unwind-tables -o sum_O2.s sum.c
grep -E '[[:space:]](q|v)[0-9]+' sum_O2.s
""")

Output

# opt.c at -O0 for AArch64 Linux: the labels and every call (bl) in the listing
$ clang --target=aarch64-linux-gnu -O0 -S -fno-asynchronous-unwind-tables -o opt_O0.s opt.c
$ grep -E '^[a-z_0-9]+:|[[:space:]]bl[[:space:]]' opt_O0.s
sum_squares:                            // @sum_squares
	bl	square
	bl	square
square:                                 // @square
forward:                                // @forward
	bl	scale
dot3:                                   // @dot3
# The same file at -O2, with directives and comments filtered out
$ clang --target=aarch64-linux-gnu -O2 -S -fno-asynchronous-unwind-tables -o opt_O2.s opt.c
$ grep -v -E '^[[:space:]]+[.]|^[.]L|^[[:space:]]*//' opt_O2.s | sed -E 's/[[:space:]]+\/\/.*$//'
sum_squares:
	mul	w8, w0, w0
	madd	w0, w1, w1, w8
	ret
forward:
	add	w0, w0, #1
	b	scale
dot3:
	ldp	w8, w11, [x1]
	ldp	w9, w10, [x0]
	ldr	w12, [x1, #8]
	mul	w8, w8, w9
	ldr	w9, [x0, #8]
	madd	w8, w11, w10, w8
	madd	w0, w12, w9, w8
	ret
# sum.c at -O2: the lines of its listing that use vector registers (q0 to q31, v0 to v31)
$ clang --target=aarch64-linux-gnu -O2 -S -fno-asynchronous-unwind-tables -o sum_O2.s sum.c
$ grep -E '[[:space:]](q|v)[0-9]+' sum_O2.s
	movi	v0.2d, #0000000000000000
	movi	v1.2d, #0000000000000000
	movi	v2.2d, #0000000000000000
	movi	v3.2d, #0000000000000000
	ldp	q4, q5, [x8, #-32]
	ldp	q6, q7, [x8], #64
	add	v0.4s, v4.4s, v0.4s
	add	v1.4s, v5.4s, v1.4s
	add	v2.4s, v6.4s, v2.4s
	add	v3.4s, v7.4s, v3.4s
	add	v0.4s, v1.4s, v0.4s
	add	v0.4s, v2.4s, v0.4s
	add	v0.4s, v3.4s, v0.4s
	addv	s0, v0.4s
	movi	v0.2d, #0000000000000000
	mov	v0.s[0], w8
	ldr	q1, [x8], #16
	add	v0.4s, v1.4s, v0.4s
	addv	s0, v0.4s

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fingerprints_session.py

Fingerprint Function What the -O2 listing shows
Inlining sum_squares no bl: the callee’s multiply in its place
Tail call forward b scale instead of bl then ret
Full unrolling dot3 the body three times, no branch back
Vectorisation sum q and v registers in a loop over int

inlining: Replacing a call with a copy of the called function's body, so that the call, the return and the passing of arguments disappear and the copy is optimised together with its caller. copied square into sum_squares: mul w8, w0, w0 squares a and madd w0, w1, w1, w8 squares b and adds, so no call is left, and square, being static, keeps no copy of its own. forward ends in b scale because scale’s result is its result: a tail call: A call that is the last thing a function does, compiled as a plain jump (b, jmp or RISC-V's tail) instead of a call and a return. The caller's return address stays in place, so the callee returns straight to the caller's caller. More in “Calls, returns and the link register”, after which scale returns straight to forward’s caller. dot3’s trip count, 3, is known while compiling, so loop unrolling: Writing a loop's body out several times so that each pass does the work of several iterations. A loop whose trip count is known while compiling can be unrolled completely, leaving no counter and no branch back. wrote the body out three times, with no counter and no branch back.

In sum.c at plain -O2, a loop that adds 32-bit integers loads 128-bit q registers and adds with add v0.4s, v4.4s, v0.4s, four additions in one instruction: auto-vectorisation: A compiler rewriting a loop written one element at a time so that each pass uses vector instructions on several elements, when it can prove the result is the same and judges that it pays.. A vectorised loop has a wide main body, a step that adds the lanes of a register together, and loops for the leftover elements. Its x86-64 and RISC-V forms differ in width and in how they finish. Whether a vector loop pays is a compiler’s cost-model decision, and two compilers can decide differently.

Four tools change what a hot loop compiles to. restrict promises the compiler that two pointers never reach the same elements. A loop pragma asks the vectoriser for a width or an interleave count. Intrinsics let C name vector instructions directly. Hand-written assembly replaces the compiler’s code for one function.

Checkpoint

Check: What -O2 leaves in a listing

  1. Question 1 of 2 At -O0, sum_squares contained two bl square. At -O2 it is mul w8, w0, w0 then madd w0, w1, w1, w8 and ret. What happened?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: square was inlined: its multiply was copied into sum_squares, once for a and once for b

    Inlining replaces a call with the callee's body. The fingerprint is a call that is gone and the callee's work in its place; square is static, so no copy of it remains either.

  2. Question 2 of 2 dot3 at -O2 loads three elements from each array, then has one mul, two madd and no branch back. Why could the compiler do this for dot3 and not for sum?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: dot3's trip count, 3, is known while compiling, so the body can be written out three times; sum's n arrives at run time

    A loop whose trip count is a small constant can be unrolled completely, leaving straight-line code. A loop whose count is a run-time value keeps a loop, though it may still be unrolled partly or vectorised.

Undefined behaviour lets the compiler delete a check

add_would_overflow tests for overflow the way the maths reads: adding a non-negative n can only make the sum smaller than x by wrapping round. But C makes signed overflow undefined behaviour: What the C standard calls an operation for which it sets no requirements at all, such as dividing by zero; a compiler may assume a correct program never performs one. More in “Division: traps, defined results, remainders”: a program whose x + n does not fit in an int has no defined meaning, so the compiler may assume it never happens. Here is the function at -O2 on three ISAs, then run on Apple silicon at -O0 and -O2:

overflow.c C · overflow.c
// overflow.c: an overflow test written the way it reads in maths, for a non-negative n: adding such an n can only
// make the sum smaller than x by wrapping round. But signed overflow is undefined behaviour in C.
int add_would_overflow(int x, int n) {
    return x + n < x;
}
add_would_overflow on three ISAs, and run at -O0 and -O2 Python · overflow_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# add_would_overflow at -O2 on three ISAs, directives and comments filtered out
clang --target=x86_64-linux-gnu -O2 -masm=intel -S -fno-asynchronous-unwind-tables -o - overflow.c | grep -v -E '^[[:space:]]+[.]|^[.]L|^[[:space:]]*#' | sed -E 's/[[:space:]]+#.*$//'
clang --target=aarch64-linux-gnu -O2 -S -fno-asynchronous-unwind-tables -o - overflow.c | grep -v -E '^[[:space:]]+[.]|^[.]L|^[[:space:]]*//' | sed -E 's/[[:space:]]+\\/\\/.*$//'
clang --target=riscv64-linux-gnu -march=rv64gc -O2 -S -fno-asynchronous-unwind-tables -o - overflow.c | grep -v -E '^[[:space:]]+[.]|^[.]L|^[[:space:]]*#' | sed -E 's/[[:space:]]+#.*$//'
# Built and run on Apple silicon at -O0, then at -O2
clang -O0 -o overflow0 overflow_main.c overflow.c && ./overflow0; echo "exit=$?"
clang -O2 -o overflow2 overflow_main.c overflow.c && ./overflow2; echo "exit=$?"
""")

Output

# add_would_overflow at -O2 on three ISAs, directives and comments filtered out
$ clang --target=x86_64-linux-gnu -O2 -masm=intel -S -fno-asynchronous-unwind-tables -o - overflow.c | grep -v -E '^[[:space:]]+[.]|^[.]L|^[[:space:]]*#' | sed -E 's/[[:space:]]+#.*$//'
add_would_overflow:
	mov	eax, esi
	shr	eax, 31
	ret
$ clang --target=aarch64-linux-gnu -O2 -S -fno-asynchronous-unwind-tables -o - overflow.c | grep -v -E '^[[:space:]]+[.]|^[.]L|^[[:space:]]*//' | sed -E 's/[[:space:]]+\/\/.*$//'
add_would_overflow:
	lsr	w0, w1, #31
	ret
$ clang --target=riscv64-linux-gnu -march=rv64gc -O2 -S -fno-asynchronous-unwind-tables -o - overflow.c | grep -v -E '^[[:space:]]+[.]|^[.]L|^[[:space:]]*#' | sed -E 's/[[:space:]]+#.*$//'
add_would_overflow:
	srliw	a0, a1, 31
	ret
# Built and run on Apple silicon at -O0, then at -O2
$ clang -O0 -o overflow0 overflow_main.c overflow.c && ./overflow0; echo "exit=$?"
add_would_overflow(INT_MAX - 50, 100) = 1
exit=0
$ clang -O2 -o overflow2 overflow_main.c overflow.c && ./overflow2; echo "exit=$?"
add_would_overflow(INT_MAX - 50, 100) = 0
exit=0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 overflow_session.py

All three listings return the sign bit of n and never read x: shr eax, 31, lsr w0, w1, #31 and srliw a0, a1, 31. With overflow ruled out, x + n < x is ordinary algebra, and subtracting x from both sides leaves n < 0.

Interactive · message by message How -O2 turns an overflow test into one shift Step through the rewrites the compiler is allowed to make. Each step is one change to the test in overflow.c.
How -O2 turns an overflow test into one shift, step 5 of 5. Messages so far: overflow.c to clang -O2: no signed overflow; clang -O2 to listing: lsr w0, w1, #31.overflow.cx + n < xclang -O2n < 0listingAArch64no signed overflowlsr w0, w1, #31-O0 printed 1, -O2 printed 0

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The test as written. add_would_overflow is meant for a non-negative n: adding such an n can only make the sum smaller than x by wrapping round past the largest int.
  2. An assumption C allows. C defines x + n only when the exact sum fits in an int. So the compiler may assume it always fits, and then x + n < x is ordinary algebra.
  3. Subtract x from both sides. In exact arithmetic, x + n < x holds exactly when n < 0. The test no longer mentions x at all.
  4. One instruction is enough. n < 0 is the sign bit of n, bit 31. lsr w0, w1, #31 shifts n right by 31 and returns that bit; x, in w0 on entry, is never read.
  5. The run that disagrees. For x = INT_MAX - 50 and n = 100 the exact sum does not fit. The -O0 build compared a wrapped sum and printed 1; the -O2 build tested only n and printed 0.

For x = INT_MAX - 50 and n = 100 the true sum does not fit. The -O0 build compared a wrapped, negative sum and printed 1; the -O2 build tested only n and printed 0. Neither is a compiler bug: C leaves the answer undefined. That is what undefined behaviour in assembly usually looks like, not a strange instruction but a missing one.

Key idea

A compiler may assume that undefined behaviour never happens, so a test that can only be true after an overflow is always false, and the listing simply leaves it out.

-fwrapv defines signed overflow to wrap. A sanitizer reports the overflow while the program runs. C23’s ckd_add asks the question directly and always gets a defined answer. An over-wide shift, and a test of a pointer made after reading through it, go wrong for the same reason.

Checkpoint

Check: Undefined behaviour and a deleted check

  1. Question 1 of 2 At -O2, add_would_overflow(int x, int n) on AArch64 is lsr w0, w1, #31 and ret. What does it return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The sign bit of n: 1 when n is negative and 0 otherwise; x is never read

    Assuming no signed overflow, x + n < x holds exactly when n < 0, and n < 0 is the sign bit of n. The compiler kept only that.

  2. Question 2 of 2 For x = INT_MAX - 50 and n = 100, the -O0 build printed 1 and the -O2 build printed 0. Why do they differ?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: -O0 computed a wrapped, negative sum and compared it with x; -O2 assumed no overflow and tested only the sign of n, which is positive

    A test that can only be true after an overflow depends on undefined behaviour. Its result can change with the optimisation level, the compiler or the surrounding code.

Practise

Copy opt.c, overflow.c and sum.c first and change only the copies. To see what a change did, paste the old and the new listing into the text diff tool.

  1. Remove static from square and compile opt.c at -O2. Is sum_squares still inlined, and what is new in the listing?
  2. Make dot3 loop to a parameter n instead of 3, and predict what replaces the mul and the madds before you compile.
  3. Rewrite the test as n > 0 && x > __INT_MAX__ - n, which never overflows. Which instructions now read x, and what do the -O0 and -O2 builds print?
  4. Compile sum.c with -fno-vectorize and compare its loop with the plain -O2 one.

Practice that adapts to you

Practise this lesson

6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 forward(int x) returns scale(x + 1), and at -O2 it ends with b scale instead of bl and ret. What is that called?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: A tail call

    A call in tail position can be compiled as a branch: the caller has nothing left to do, so the callee's return can go straight back to the caller's caller.

  2. Question 2 of 6 A loop over an int array at -O2 contains ldp q4, q5, [x8, #-32] and add v0.4s, v4.4s, v0.4s. What does that tell you?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: The loop was vectorised: each add does four 32-bit additions in one 128-bit register

    Vector registers in a loop whose source works on one element at a time are the fingerprint of auto-vectorisation.

  3. Question 3 of 6 You remove static from square in opt.c and compile at -O2 again. What do you expect in the listing?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: sum_squares is still inlined, and square also gets a body of its own, because another file might call it

    A static function used only by inlined calls needs no copy of its own. An external one keeps a copy for other files, even when every call in its own file was inlined.

  4. Question 4 of 6 What does C say about an int addition whose exact result does not fit in an int?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: It is undefined behaviour: the standard sets no requirements for the program at all

    Unsigned arithmetic wraps by definition; signed overflow is undefined. A test that relies on a signed sum wrapping is therefore not portable C.

  5. Question 5 of 6 Why does the -O2 listing of add_would_overflow never read x?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: Assuming no overflow, x + n < x is equivalent to n < 0, which does not involve x

    Undefined behaviour gives the compiler a fact to use: the sum fits. With that fact, the comparison simplifies until x drops out.

  6. Question 6 of 6 Which change makes the overflow test give the same answer at -O0 and at -O2?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Compare before adding, as in n > 0 && x > __INT_MAX__ - n, so no addition can overflow

    An overflow test must not overflow. Compare against the limit before adding, or ask for the result with a checked operation.

Vectorised loops read lane by lane on three ISAs Pro: opens with a Pro, Premium or Ultimate pass

A vectorised loop is usually three loops in one function: a wide main loop, a narrower one for most of what is left, and a scalar tail. This part reads all three in the AArch64 listing of sum, compares the x86-64 and RISC-V forms, and weighs what the vector loop is worth in two ways.

In this part:

  • Sixteen elements an iteration on AArch64, then the leftovers
  • The same loop with SSE2, AVX2 and the RISC-V vector extension
  • Why GCC declined at -O2, and what a cost model weighs
  • What the vector loop buys, in a model of the core

1 diagram · 3 runnable examples (Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Undefined behaviour that removes checks, reproduced and fixed Pro: opens with a Pro, Premium or Ultimate pass

Three tests that look correct in C depend on undefined behaviour, and each can be made to fail, explained from its listing and fixed. Every fix here is shown the same way: the old code gives the wrong answer and the new code the right one, at every optimisation level.

In this part:

  • The overflow test with -fwrapv, a sanitizer and checked arithmetic
  • A shift by the full width, caught and fixed
  • A null test after the read, and the order that keeps it

6 runnable examples (C, Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Restrict, pragmas, intrinsics or assembly: choosing for a hot loop Pro: opens with a Pro, Premium or Ultimate pass

Each tool that changes a hot loop’s code asks something different of you. This part reads what restrict removes from a listing, measures hand-written intrinsics against the compiler’s own loop in one model, and ends with a choice made from your own case.

In this part:

  • The overlap test that restrict removes
  • Intrinsics against the vectoriser’s own loop
  • Choosing a tool for a hot loop

4 runnable examples (C, Python) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

A warm-up for freshers: what does -O2 turn on that -O0 leaves off?

-O0 compiles each statement on its own and keeps every variable in memory, so a debugger can follow the source line by line. -O2 turns on the optimisations that almost always pay: values stay in registers, small functions are inlined, calls in tail position become jumps, constants are folded, and loops are unrolled and vectorised when a cost model says it pays, by Clang and by GCC since version 12. It also uses the language’s rules to the full, assuming no signed overflow and no read through a null pointer, so checks that rely on either can vanish. GCC keeps costlier transformations for -O3.

These three go further: an overflow check that fails only at -O2, proof that a loop was vectorised, and intrinsics against the compiler.

  1. Mid to senior

    A C function rejects bad input with if (a + b < a) return -1; for int a and a non-negative int b. Its unit test of an overflowing pair passes in the debug build and fails in the -O2 release build. Explain what happened, how you would show it, and how you would fix it for every build.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A library's README says its hot summing loop is vectorised at -O2. Your product builds it with Clang on one platform and GCC on another. How do you confirm the claim for both builds, and what do you check if one of them is not vectorised?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A teammate rewrote a summing loop with NEON intrinsics and expects it to beat the compiler's code. How do you decide whether the rewrite helps, and why could it be slower than what the compiler produced from plain C?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What four fingerprints of optimisation can you spot in a listing?

    A call that vanished is inlining; a final call turned into a branch is a tail call; a loop body written out several times is unrolling; vector registers working on a loop written for one element at a time are vectorisation.

  2. Why can -O2 compile the test x + n < x into a check of the sign of n?

    Signed overflow is undefined behaviour, so the compiler may assume it never happens. Without overflow, x + n < x holds exactly when n is negative, and the listing tests only that.

  3. What does -O2 turn on that -O0 leaves off?

    Values kept in registers, inlining of small functions, tail calls as jumps, constant folding, and unrolling and vectorising of loops when a cost model says it pays. It also relies on the language's rules, such as no signed overflow, so checks that depend on breaking them can vanish.

  4. What does inlining do to a call?

    It replaces the call with a copy of the called function's body, so the call, the return and the argument passing disappear and the copy is optimised together with its caller.

  5. What is loop unrolling, and what happens when the trip count is known while compiling?

    Writing the body out several times so each iteration does the work of several. With a known trip count the loop can be unrolled completely, leaving no counter and no branch back.

Key takeaways

  • An optimised listing is read by what is missing or repeated: a vanished call is inlining, a final call turned into a branch is a tail call, a loop written out is unrolling, and vector registers in a scalar loop are vectorisation.
  • Signed overflow is undefined behaviour, so -O2 may compile x + n < x to a test of the sign bit of n.
  • A test that relies on undefined behaviour can change its answer with the optimisation level, and the listing shows it as a missing check.

Terms in this lesson

auto-vectorisation
A compiler rewriting a loop written one element at a time so that each pass uses vector instructions on several elements, when it can prove the result is the same and judges that it pays.
inlining
Replacing a call with a copy of the called function's body, so that the call, the return and the passing of arguments disappear and the copy is optimised together with its caller.
loop unrolling
Writing a loop's body out several times so that each pass does the work of several iterations. A loop whose trip count is known while compiling can be unrolled completely, leaving no counter and no branch back.
pointer aliasing
Two pointers reaching the same memory. A compiler that cannot rule it out must keep loads and stores through them in source order, which can stop it from vectorising a loop.
undefined behaviour sanitizer
UndefinedBehaviorSanitizer, the -fsanitize= checks of GCC and Clang that compile extra tests into a program to report undefined behaviour, such as a signed overflow, while it runs.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.