Skip to content

Extended Inline Assembly

Inline assembly has two contracts: the processor instructions and the compiler's model of their effects. Start with one register-only expression before adding memory or control flow.

Before you start

Complete NEON and scalar tails. Basic C++ is needed; use the separate-function interop lesson first if declarations or fixed-width types are unfamiliar.

Outcome: describe inputs, outputs and clobbers of a GCC extended-asm statement and test it at multiple optimization levels. Use a fresh directory on the Pi.

Describe every observable effect

The format is asm(template : outputs : inputs : clobbers). Operand constraints tell the compiler where values may live and whether an operand is read, written or both. Merely naming a register in a string does not inform the optimizer about an undeclared effect.

For the example below, "=r" declares a register output and "r" declares each input. %x[name] requests the 64-bit register spelling. The single ADD reads its inputs before writing the output, so an output sharing an input register is allowed.

Build a small checked example

// file: inline.cpp
#include <cstdint>
#include <iostream>
#include <limits>

static std::uint64_t add_inline(std::uint64_t a, std::uint64_t b)
{
    std::uint64_t result;
    asm ("add %x[out], %x[left], %x[right]"
         : [out] "=r" (result)
         : [left] "r" (a), [right] "r" (b));
    return result;
}

int main()
{
    const std::uint64_t values[] = {0, 1, 17,
        std::numeric_limits<std::uint64_t>::max()};
    for (auto a : values)
        for (auto b : values)
            if (add_inline(a, b) != a + b) return 1;
    std::cout << "PASS inline: 16 unsigned input pairs\n";
}
1
2
3
4
5
6
7
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -O0 inline.cpp -o inline-O0
./inline-O0
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -O2 inline.cpp -o inline-O2
./inline-O2
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -O3 inline.cpp -o inline-O3
./inline-O3
objdump -d inline-O2 | less

Every run should print:

PASS inline: 16 unsigned input pairs

No flags clobber is needed for this ADD form because it does not set NZCV. No memory clobber is needed because the statement accesses no memory. Its result is used, so volatile is not required simply to force a useless operation to remain.

Know when this model stops being sufficient

Change New question
Read and overwrite one operand Does it need a read/write constraint?
Write an output before reading all inputs Is early-clobber required?
Change NZCV Is the flags clobber declared?
Read or write pointed-to storage Which memory effects are represented?
Call another function Are stack, LR and all call effects modeled?
Communicate between threads/devices Which language and hardware ordering rules apply?

Do not hide a function call inside an otherwise register-only template. A compiler memory clobber is not a CPU memory barrier, and volatile is not a synchronization policy. See the GCC extended-asm documentation before extending the contract.

Prefer the simplest correct boundary

C++ a + b already expresses this unsigned operation. Inline asm here teaches constraints; it is not an optimization recommendation. For multiple blocks, calls or a reusable algorithm, a separate .S function with explicit tests is often easier to inspect.

If optimized builds disagree, investigate constraints and undefined behavior before disabling optimization. Passing three builds is useful evidence, not proof that an incomplete constraint set is valid.

Check your understanding

Why is adding volatile not a repair for a missing output or memory effect?

Compare your reasoning

Volatile does not declare the missing dependency or access. The compiler still needs an accurate operand/clobber contract to generate correct surrounding code.

Continue the course

Previous: NEON and Scalar Tails · Course roadmap · Next: Reading Specifications and Measuring

Donate