Inline Assembly
By the end of this lesson you'll be able to read a basic GCC/Clang inline-assembly statement, explain its output, input, and clobber operands, and — far more importantly — know why compiler intrinsics and the optimizer are almost always the better tool, plus the portability and safety traps that make hand-written asm a last resort.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
Think of the compiler as a master translator who turns your C++ into flawless machine code and re-checks the whole document every time anything changes. Writing inline assembly is like grabbing the pen and scribbling a sentence in the target language yourself . Occasionally you know a word the translator's dictionary is missing — but the moment you write it, the translator stops re-checking that sentence. If you misspell a register or forget to declare what you changed, nobody catches it, and the error can hide until a different optimization level exposes it. Compiler intrinsics are the polite middle ground: you suggest the exact word, but the translator still owns the page — it stays portable and keeps getting proofread.
1. What Inline Assembly Is (and Isn't)
Inline assembly means writing raw CPU instructions directly inside a C++ function. On GCC and Clang you use the asm keyword; on MSVC the syntax differs again — already a portability headache. It exists for the handful of cases the language genuinely cannot express: a privileged instruction in an operating-system kernel, a brand-new CPU feature with no wrapper yet, or constant-time cryptographic code. For everything else, the compiler writes better assembly than you will. Study the worked example below — the asm is in comments, and the runnable C++ computes the identical answer.
2. Extended Asm Syntax: Outputs, Inputs & Clobbers
A GCC/Clang extended asm statement has four colon-separated parts: the instruction template, the output operands (what the asm writes), the input operands (what it reads), and the clobber list (registers and flags it trashes). Placeholders %0 , %1 , %2 in the template refer to those operands in order. The keyword asm volatile tells the optimizer "keep this exactly where it is — do not move or delete it". Read the anatomy below carefully; the operand model is the whole game.
🔎 Deep Dive: reading the colons
The shape is always asm volatile("template" : outputs : inputs : clobbers) . Each operand is a constraint string plus a C++ variable in parentheses. The constraint tells the compiler where the value can live and whether the asm reads it, writes it, or both.
Get the clobber list right and the optimizer keeps working around your block safely. Get it wrong and it assumes a register or memory is untouched, reuses it, and your program corrupts data — usually only at higher optimization levels.
3. The Better Tool: Compiler Intrinsics
A compiler intrinsic looks like a normal function call but compiles down to a single CPU instruction. Examples: __builtin_popcount(x) counts set bits, __builtin_ctz(x) counts trailing zeros, and C++20's <bit> header gives portable std::popcount and std::countr_zero . The crucial difference from inline asm: the optimizer understands an intrinsic, so it can fold, reorder, and schedule it, and it stays portable across compilers. The example below uses a plain-C++ popcount so it runs here, with the real intrinsic shown in comments.
Your turn. The program below is almost complete — fill in the two blanks marked ___ using the hints in the comments, then run it and check the expected output.
4. SIMD & Letting the Optimizer Win
SIMD (Single Instruction, Multiple Data) processes several values with one instruction — SSE does 4 floats at a time, AVX does 8. You can hand-write it with intrinsics like _mm256_add_ps from <immintrin.h> , but the easiest and most portable path is to write a plain loop and compile with -O3 -march=native : modern compilers auto-vectorize it into exactly those wide instructions. The runnable loop below is one the optimizer will happily turn into AVX for you.
Pro Tips
- 💡 Profile before you reach for asm: measure with a profiler and read the compiler's output ( -S or Compiler Explorer) first. Most "slow" code is fixed by a better algorithm, not assembly.
- 💡 Prefer the C++20 <bit> header: std::popcount , std::countl_zero , std::bit_ceil are portable, type-safe, and need no compiler-specific builtins.
- 💡 Let -O3 -march=native vectorize: a clean loop the auto-vectorizer can see usually beats hand-written intrinsics and is far easier to maintain.
- 💡 If you must write asm, always use volatile for side-effecting blocks and list every clobbered register, plus "memory" and "cc" when relevant.
Common Errors (and the fix)
- Clobbering registers you didn't declare: your asm writes rcx but you left it out of the clobber list. The compiler keeps using its old value and data corrupts — often only at -O2 . Fix: list every changed register, plus "memory" and "cc" when you touch RAM or flags.
- Non-portable code: asm("addl %1, %0" ...) assembles on x86 but fails or means something else on ARM or with MSVC. Fix: gate asm behind architecture checks, or — much better — replace it with an intrinsic or plain C++ that every compiler supports.
- Breaking the optimizer: the compiler can't see inside an asm block, so it can't fold constants or reorder around it, and your "fast" hack ends up slower than the C++ it replaced. Fix: prefer intrinsics the optimizer understands; reserve asm for what truly can't be expressed otherwise.
- Undefined behaviour from a missing volatile or wrong constraint: without volatile the optimizer may delete a side-effecting block; a "=r" on a value you actually read is a lie to the compiler. Both are UB and may "work" until you change a flag. Fix: mark side-effecting asm volatile and use "+r" for read-and-write operands.
- "impossible constraint in 'asm'" / "operand number out of range": the template references a %2 you never supplied, or a constraint can't be satisfied. Fix: count your operands — %0 is the first output — and make every placeholder match a declared operand.
📋 Quick Reference
Concept
Form
Means
Extended asm
asm volatile(t : out : in : clob)
four colon sections
Output operand
"=r"(x)
asm writes x (a register)
Input operand
"r"(x)
asm reads x
Read+write
"+r"(x)
asm reads and writes x
Clobbers
: "memory", "cc"
touched RAM / flags
Portable bit op
std::popcount(x)
C++20, no asm needed
Auto-vectorize
-O3 -march=native
compiler emits SIMD
Frequently Asked Questions
Mini-Challenge: Alignment Checker
No blanks this time — just a brief and a blank canvas (with an outline to keep you on track). Use the provided trailingZeros helper — the portable twin of the __builtin_ctz intrinsic — to decide whether an address is 16-byte aligned. Build it, run it, and check your output against the example in the comments.
🎉 Lesson Complete
- ✅ Inline assembly embeds raw CPU instructions; it's a last resort , not a default
- ✅ Extended asm has four parts: template : outputs : inputs : clobbers , with %0/%1 by position
- ✅ asm volatile pins the block in place; the clobber list keeps your promise to the optimizer
- ✅ Compiler intrinsics ( __builtin_popcount , C++20 <bit> ) give the same instruction, portably
- ✅ For SIMD, a clean loop plus -O3 -march=native auto-vectorizes and usually wins
- ✅ Top traps: clobbering, non-portability, breaking the optimizer, and undefined behaviour
- ✅ Next lesson: C++ Networking — talk to other machines over sockets
Practice quiz
What is inline assembly?
- A faster C++ compiler
- A standard library header
- Raw CPU instructions written directly inside a C++ function
- A type of smart pointer
Answer: Raw CPU instructions written directly inside a C++ function. Inline assembly embeds raw, architecture-specific CPU instructions inside C++ code. On GCC/Clang you use the asm keyword.
How many colon-separated sections does a GCC/Clang extended asm statement have?
- Four: template, outputs, inputs, clobbers
- Two: template and outputs
- One: just the template
- Three: inputs, outputs, return
Answer: Four: template, outputs, inputs, clobbers. Extended asm is asm volatile("template" : outputs : inputs : clobbers) — four sections. %0, %1, ... refer to operands in order.
What does the 'volatile' in asm volatile do?
- Makes the asm run twice
- Marks the registers as 32-bit
- Allocates heap memory
- Tells the compiler not to delete or move the asm block, even if outputs look unused
Answer: Tells the compiler not to delete or move the asm block, even if outputs look unused. volatile pins the block in place so the optimizer won't assume it's pure and remove it — essential for side-effecting instructions.
What is the clobber list for?
- Listing input variables
- Naming every register, plus "memory" or "cc", that the asm modifies but didn't declare as an output
- Setting the optimisation level
- Declaring the return type
Answer: Naming every register, plus "memory" or "cc", that the asm modifies but didn't declare as an output. Clobbers keep your promise to the optimizer. Forget one and the compiler reuses that register believing its old value is valid, causing corruption.
What is a compiler intrinsic?
- A function-shaped call the compiler turns into a single CPU instruction, which the optimizer still understands
- A macro that expands to asm text
- A runtime library call
- A debugging tool
Answer: A function-shaped call the compiler turns into a single CPU instruction, which the optimizer still understands. An intrinsic like __builtin_popcount(x) compiles to one instruction but stays portable and visible to the optimizer — far safer than inline asm.
Which C++20 header provides portable bit operations like std::popcount and std::countr_zero?
- <bitset>
- <cstdint>
- <bit>
- <immintrin.h>
Answer: <bit>. C++20 added <bit> with portable std::popcount, std::countl_zero, std::countr_zero, and std::bit_ceil — no compiler-specific builtins needed.
What does popcount of the value 0b1011 return?
- 2
- 3
- 4
- 11
Answer: 3. popcount counts the bits set to 1. 0b1011 has three 1-bits, so the result is 3.
What does SIMD stand for?
- Simple Integer Memory Data
- Synchronous Inline Machine Directives
- Static Inline Method Dispatch
- Single Instruction, Multiple Data
Answer: Single Instruction, Multiple Data. SIMD = Single Instruction, Multiple Data: one instruction operates on several values at once (SSE does 4 floats, AVX does 8).
What is the easiest, most portable way to get SIMD speedups for a plain loop?
- Hand-write _mm256 intrinsics always
- Write a clean loop and compile with -O3 -march=native so the compiler auto-vectorizes
- Use std::vector only
- Add the 'register' keyword
Answer: Write a clean loop and compile with -O3 -march=native so the compiler auto-vectorizes. A clean loop the auto-vectorizer can see, built with -O3 -march=native, usually matches hand-written intrinsics and is far easier to maintain.
When is hand-written inline assembly actually justified?
- Whenever code feels slow
- For every hot loop
- Almost never — only for things the language can't express, after profiling proves a need
- To replace std::vector
Answer: Almost never — only for things the language can't express, after profiling proves a need. Inline asm is a last resort for privileged instructions, a CPU feature with no intrinsic, or constant-time crypto. Otherwise the compiler writes better asm.
Continue this course
- Previous: Undefined Behavior
- Next: C++ Networking