Computer Architecture · Instruction Sets

Immediate Encoding and How Arbitrary Constants Get Materialized

Why an instruction can hold only a small constant, how sign extension and scaled fields are packed into the word, and the instruction sequences that build a full-width value.

A constant in an instruction competes for the same bits as the register numbers, so it is always smaller than the register the value will be compared against. A 64-bit machine that can only encode a twelve-bit immediate cannot put an arbitrary 64-bit constant into a single instruction, and every such constant must be assembled from pieces. The assembly is the compiler’s job, but the shape of the pieces is the ISA’s, and the shape determines both the instruction count and a class of subtle correctness bugs around sign.

The immediate is signed until proven otherwise

Nearly every immediate field in the common ISAs is sign-extended from its top bit before use. A twelve-bit field holding 0x7FF is 2047; the same field holding 0x800 is −2048. The sign extension applies to arithmetic immediates, load displacements, and branch offsets alike, and it applies even when the operation is logically unsigned, because the field itself carries no type.

This produces the first of the recurring traps. A programmer who wants to add 2048 to a register cannot use the twelve-bit immediate form, because 2048 is not representable; the assembler will either reject the literal or silently encode the low twelve bits and produce −2048. Adding a large positive constant requires materialising it in a register first. The assembler’s behaviour differs between tools, which is why the failure can appear as a wrong result at run time rather than a diagnostic at assembly time.

AArch64 handles the same problem by scaling the immediate for some operations. Logical immediates are encoded as a compact pattern rather than as a plain number: a small field selects a run of ones and a rotation, so a thirteen-bit encoding can express values such as 0xFF00FF00FF00FF00 that would need many bits as a literal. The encoding is called a bitmask immediate, and it covers a large fraction of the constants that appear in masks and alignment operations. Constants outside the pattern need a register.

Building a wide constant from narrow pieces

The general solution is a two-step sequence: load the upper part of the constant into a register, then combine it with the lower part.

RISC-V’s lui places a twenty-bit value in bits 31:12 of the destination register and zeroes the low twelve bits. Its companion addi adds a twelve-bit signed immediate. The pair is intended to build a 32-bit constant, and the sign extension of addi is what makes the pairing delicate: lui sets bits 31:12 to imm20, and if addi’s immediate is negative, the addition borrows from bit 12, so the upper part must be incremented by one to compensate.

/* Assemble a 32-bit constant as lui + addi, the way a compiler must. */
static void emit_const32(uint32_t value, int rd)
{
    int32_t lo = (int32_t)(value << 20) >> 20;   /* sign-extend low 12 bits */
    uint32_t hi = (value - (uint32_t)lo) >> 12;  /* upper 20 bits after the borrow */
    emit_lui(rd, hi);
    if (lo != 0)
        emit_addi(rd, rd, lo);
}

The borrow adjustment is not an optimisation. Without it, a constant whose low twelve bits have the top bit set is off by 4096. A compiler that emits lui with the plain upper bits and then addi with the raw lower bits produces a value that is wrong for half of all constants, and wrong in a way that only shows up for the specific values tested.

The alternative on RISC-V is the li pseudo-instruction, which the assembler expands into the same sequence with the same correction, plus a shift-and-add path for constants that fit in a shifted twelve-bit pattern. The expansion is not always two instructions: a constant with a long run of zeros can sometimes be built in one, and one with scattered bits needs the full sequence. The count is data-dependent, which matters for code size estimation and for anything that measures instruction counts.

AArch64 uses a different decomposition: movz moves a sixteen-bit value into a chosen sixteen-bit lane with the rest zeroed, movk keeps the register and replaces one lane. A 64-bit constant is built with up to four instructions, one lane at a time, and the assembler chooses movz for the first non-zero lane and movk for the rest. The scheme has the advantage that each step is independent of the others and no borrow adjustment is needed, at the cost of needing up to four instructions where the lui pair needs two for a 32-bit value.

x86 takes the most direct route because its instructions are variable length: a mov with a register destination and an immediate can carry a full 32-bit immediate, and in 64-bit mode a sign-extended 32-bit immediate into a 64-bit register. A full 64-bit constant needs a movabs form with eight immediate bytes, and the encoding is legal but large. The trade is explicit: spend instruction bytes on the constant rather than instructions on the reconstruction.

Shifted immediates and the cost of the multiply

A third encoding family folds a rotate into the immediate. ARM’s A32 encodings do this: the immediate field is twelve bits, of which eight hold a value and four hold a rotation amount applied in even steps, so a constant that is a small value rotated into position is representable in one instruction. The restriction is that the constant must have a specific structure — a run of bits that fits in eight bits after rotation — and constants without that structure fall back to the multi-instruction sequence.

The encoder for such a format must search. Given a 32-bit constant, the assembler tries each of the sixteen rotations of the eight-bit value and checks whether the result fits. The search is bounded and cheap at assembly time, and the payoff is one instruction instead of two for a class of constants that compilers generate frequently: alignment masks, bit-field masks, and small powers of two scaled by a structure size. The cost is in the decoder, which must apply the rotation before the operation, lengthening the path from instruction fetch to ALU input by the width of a barrel shifter.

The shift amount itself is sometimes an immediate and sometimes a register. A variable shift needs a register field, which competes with the register that holds the value being shifted. An ISA with two source register fields and a shift-immediate field has to choose where the shift amount lives, and the choice is visible in the encoding: x86 puts the shift count in a fixed register (cl) for variable shifts, which is a register-file port decision disguised as an encoding decision.

Immediates that are addresses

Branch and jump displacements are immediates with an extra twist: they are relative to the program counter, and their width determines how far a branch can reach.

A twelve-bit branch displacement in units of two bytes reaches ±4 KiB. A thirteen-bit displacement in the same units reaches ±8 KiB. A nineteen-bit displacement in units of four bytes reaches ±1 MiB, which is the range of an AArch64 conditional branch; RISC-V’s conditional branches reach only ±4 KiB, a thirteen-bit displacement in units of two bytes. The unconditional jump formats go further: AArch64’s b carries a twenty-six-bit displacement in units of four bytes, reaching ±128 MiB, and RISC-V’s jal carries a twenty-one-bit signed displacement in units of two bytes, reaching ±1 MiB.

The reach is not an abstract property. A function larger than the branch range needs its conditional branches rewritten as an inverted branch around an unconditional jump, and the compiler must know the range to decide. A hot loop that exceeds the range pays an extra jump on the not-taken path, which is exactly the path that should be cheap. Position-independent code makes this worse, because the linker cannot always place a call target within range and must insert a trampoline, a stub that loads the full address and jumps to it.

The relative form is preferred over the absolute form for two reasons. The displacement field can be narrower than an address because it only needs to cover the distance between two nearby instructions, and the code becomes position-independent: moving the whole text segment does not change any branch. The cost is that the linker must resolve every branch at link time or load time, and a branch whose target is in another shared object cannot be resolved to a displacement at all.

Immediates in memory operations

A load’s immediate is a displacement added to a base register, and the same width constraints apply. The common case is a field access at a small offset, which fits. The uncommon case is a large stack frame or a large structure, where the offset exceeds the field and the compiler must compute a new base.

The compiler’s usual response is to allocate a scratch register and add the large offset into it, then load with a zero displacement. That costs one instruction and one register, and the register pressure is what makes large stack frames expensive rather than the instruction itself. A function with a 100 KiB stack frame on an ISA with a twelve-bit load displacement needs a scratch register for nearly every local access, and the register allocator may then spill other values to make room.

Scaled displacements change the arithmetic. If the immediate is in units of the access size, a twelve-bit field reaches 4095 bytes for a byte access and 32760 bytes for an eight-byte access. The scale is free to encode because the low bits of an aligned address are always zero, and the restriction is that the effective address must be aligned to the access size. A misaligned access with a scaled displacement cannot be expressed, so the compiler must form the address in a register, and the alignment requirement becomes visible in the code.

Rounding and overflow at the boundary

The failure modes around immediates cluster at three boundaries.

The first is the representable range. A value just outside the signed range of a field is the most dangerous case, because the assembler may accept it and the encoded result is the two’s complement wraparound. A value far outside the range is usually rejected. Testing at the boundary — 2047 and 2048, −2048 and −2049 — is the only reliable way to find these.

The second is the interaction of the immediate with the operation’s width. An addi on a 64-bit register sign-extends its twelve-bit immediate to 64 bits and then adds. The result is correct for a negative immediate and correct for a positive one, but the carry out of bit 63 is discarded, so adding a negative immediate to a small positive value wraps in the expected way. The same immediate used in a logical operation is not sign-extended on some architectures, which means the same field value has different meaning in adjacent instructions of the same format.

The third is the assembler’s pseudo-instruction layer. A pseudo-instruction like li may expand to a different number of instructions depending on the value, so a hand-written assembly routine that assumes a fixed size is wrong for some constants. Linkers that relax or compress branches have the same property: the size of a sequence is decided after layout, which is why relaxation is iterative and why the iteration has to be shown to terminate.

A related asymmetry applies to the sequence a comparison turns into. Materialising a large constant and then testing it costs two instructions where a machine with a flag-setting immediate comparison needs one, which is one of the reasons Condition Codes, Flag Registers and the Dependency They Create notes that flag-writing instructions folded into arithmetic save space even when they add a dependency.

The encoding of a constant is a small problem with a long tail. The width of the field is settled by the format, and everything after that — the borrow correction, the lane selection, the branch range, the scratch register — is a consequence that every compiler must implement identically or produce code that is subtly wrong.

References

  1. The RISC-V Instruction Set Manual, Volume I: Unprivileged ISA — RISC-V International
  2. Arm Architecture Reference Manual for A-profile architecture — Arm Limited
  3. Intel 64 and IA-32 Architectures Software Developer Manuals, Volume 2 — Intel Corporation