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.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
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.
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.
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 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 , 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.
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.
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.