Compiler Optimization: ARM's Barrel Shifter
One of the recurring themes in compiler optimization is that the best strategy depends entirely on the target architecture. What is optimal on x86 can look very different on ARM, and the barrel shifter is a perfect illustration of that principle.
What Is the Barrel Shifter?
On ARM, most arithmetic and logical instructions accept a shift applied directly to the second operand. This is not a separate instruction —it is an optional modifier baked into the instruction encoding itself. The hardware that performs this shift is called the barrel shifter, and it operates at essentially zero additional cost for small shift amounts on modern cores.
A concrete example:
add w0, w0, w0, lsl #1 ; w0 = w0 + (w0 << 1), which equals w0 * 3
A single add instruction computes x + (x * 2) without any dedicated shift step. On a modern Cortex-A76, left shifts of four bits or fewer combined with add or sub have no throughput penalty at all.
How ARM Compilers Handle Constant Multiplication
Because ARM has this capability, compilers targeting AArch64 rarely reach for a multiply instruction when the multiplier is a small integer. The table below shows what GCC or Clang typically emit:
| Expression | Generated assembly | Notes |
|---|---|---|
x * 2 | lsl w0, w0, #1 | Simple left shift |
x * 3 | add w0, w0, w0, lsl #1 | x + (x << 1) |
x * 4 | lsl w0, w0, #2 | Left shift by 2 |
x * 16 | lsl w0, w0, #4 | Left shift by 4 |
x * 25 | mov w1, #25 / mul w0, w0, w1 | Falls back to mul |
x * 522 | mov w1, #522 / mul w0, w0, w1 | Falls back to mul |
Constants like 25 and 522 cannot be decomposed into a small number of shift-and-add operations that beat a single mul, so the compiler loads the constant into a register and multiplies directly. ARM’s 32-bit fixed instruction width limits immediate encoding space, which is why large constants must be materialized with mov first.
A 32-bit ARM Trick: rsb
ARMv7 (32-bit ARM) had an additional instruction that made this even more expressive: rsb, or Reverse Subtract. While a normal sub computes op1 - op2, rsb computes op2 - op1. Combined with a shifted second operand, this enables a single instruction to compute (2^n - 1) * x:
mul_by_7:
rsb r0, r0, r0, lsl #3 ; r0 = (r0 << 3) - r0 = 8x - x = 7x
bx lr
This handles multiplications by 3, 7, 15, 31, and similar values in one instruction. The AArch64 architecture removed rsb —a deliberate simplification of the ISA that trades this niche capability for a cleaner design.
Contrast with x86
On x86, the analogous tool is the lea instruction (Load Effective Address). Although designed for address computation, lea can perform a + b * k where k is 1, 2, 4, or 8, and it writes to an arbitrary output register without modifying flags. Compilers use it for small-constant multiplication on x86 in exactly the same way ARM uses shifted add.
The key distinction is that ARM’s approach integrates more cleanly into the instruction stream: the shift is part of the arithmetic instruction itself, rather than a separate address-computation instruction pressed into service. On the other hand, lea on x86 can combine a base, an index, and a displacement in one step, which ARM’s model does not match as directly.
What This Means for Source Code
The practical implication is straightforward: write x * 3, not x + (x << 1). Compilers know the target architecture’s performance model in detail. For x86 they will emit lea, for ARM they will emit a shifted add. Hand-coded bit manipulation not only hurts readability —it can also prevent the compiler from recognizing the pattern and choosing the genuinely best instruction sequence.
Compiler Explorer makes this easy to verify. Compile a function with x * 7 for AArch64 at -O2 and observe the output. Then try writing the “optimized” version manually and watch the compiler either match what you wrote or, more likely, produce something better.
Summary
ARM’s barrel shifter is one of the cleaner examples of how ISA design shapes compiler output. By embedding shift capability inside data-processing instructions, ARM gives compilers a zero-cost tool for constant multiplication that is both compact and fast. The compiler handles all of this automatically —your job is to express intent clearly in source code and let the optimizer do its work.