Back to Home

Assembler Optimization: How One Letter Speeds Up Code 3x

Experimental Study Demonstrates How Replacing 16-Bit Operations with 32-Bit Ones in Assembler Eliminates False Dependencies Between Registers, Speeding Up Division Threefold. Analysis of Register Alias Table Operation and Pipelining Features.

Secret to 3x Speedup: Microoptimization in Assembler
Advertisement 728x90

# How Replacing One Letter in Assembler Speeds Up Division Threefold: A Technical Breakdown

Changing a single character in an assembly instruction cuts division operation time by 66%. Real-world benchmarks on modern CPUs show how false register dependencies turn out-of-order execution potential into a bottleneck. We break down the x86-64 architectural nuances affecting performance.

Architectural Features of x86-64 Registers

Modern x86-64 processors use a register hierarchy with an inherited structure. The rax register (64 bits) includes:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           rax (64 bit)                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚          (verkhnie 32)         β”‚           eax (32 bit)        β”‚
β”‚                               β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                               β”‚               β”‚   ax (16 bit) β”‚
β”‚                               β”‚               β”œβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                               β”‚               β”‚ah (8) β”‚al (8) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜

Key point: Writing to lower parts of the register (ax, al) doesn't affect upper bits, while operations with 32-bit registers (eax) completely overwrite the upper 32 bits. This creates hidden dependencies critical for pipelining.

Google AdInline article slot

The div/idiv division instructions use a pair of registers:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Size   β”‚ Delimoe        β”‚ Chastnoe in    β”‚ Balance in           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 16-bit   β”‚ dx:ax          β”‚ ax           β”‚ dx                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 32-bit   β”‚ edx:eax        β”‚ eax          β”‚ edx                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 64-bit   β”‚ rdx:rax        β”‚ rax          β”‚ rdx                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Before division, the upper part of the pair must be zeroed. Standard pattern:

mov edx, 0
mov eax, 536700
div dword [denominator]

Experimental Data: 3.7Γ— Difference

The test loop processed 2,073,600 iterations (1920Γ—1080), simulating pixel rendering. Results in cycles per iteration (after subtracting loop overhead):

Google AdInline article slot
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        β”‚ idiv (clean.) β”‚ div (clean.)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 16-bit β”‚ 22.2         β”‚ 22.2         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 32-bit β”‚  6.0         β”‚  6.0         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 64-bit β”‚ 26.2         β”‚ 24.1         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Surprise: 32-bit operations run 3.7 times faster than 16-bit ones with identical input data. The reason isn't division algorithm complexity, but microarchitectural execution details.

Mechanism of False Dependencies

Processors use the Register Alias Table (RAT) to map logical registers to physical ones. Notably:

  • Writing to a 32-bit register (mov edx, ...) completely overwrites the value, breaking dependencies with prior operations
  • Writing to a 16-bit register (mov dx, ...) requires merging new data with upper bits, creating a false dependency

In the 16-bit variant, the dependency chain looks like this:

Google AdInline article slot
div word β†’ merge β†’ mov dx β†’ merge β†’ div word β†’ ...

Each iteration must wait for the previous one due to register merging needs. In the 32-bit variant, this chain breaks, enabling the processor to pipeline execution.

Key Performance Metrics

  • Latency: 21–22 cycles for 16-bit div (time until result ready)
  • Throughput: 6 cycles for 16-bit div (dispatch interval between operations)

When operations depend on each other, latency is the bottleneck. For independent opsβ€”throughput. The 22/6 β‰ˆ 3.7 ratio explains the speedup.

Hypothesis Check: Replacing One Letter

The experiment confirmed the theory. Replacing 16-bit instructions with 32-bit ones in a 16-bit context:

; Before
mov dx, 0x0008
mov ax, 0x2B7C

; After
mov edx, 8
mov eax, 0x2B7C

Led to dramatic results:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       β”‚ idiv (clean.) β”‚ div (clean.)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 16-bit (mov dx)       β”‚ 22.2         β”‚ 22.2         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 16-bit (mov edx)      β”‚  7.0         β”‚  7.0         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

One letter e in the instruction eliminated the false dependency, dropping execution time from 22.2 to 7.0 cyclesβ€”right to the divider's throughput level.

What’s Important

  • Register size choice is critical: Using 32-bit ops to prep 16-bit division eliminates unnecessary dependencies
  • Latency vs Throughput: Dependencies make performance latency-bound, not throughput-bound
  • Architectural nuances: Understanding RAT and partial register merging is essential for low-level optimization
  • Divider produces remainder faster: x86 computes quotient and remainder in parallel, allowing earlier use of edx over rax
  • Testing is mandatory: Theoretical estimates often diverge from real measurements due to microarchitectural quirks

Practical Recommendations for Developers

  • For division, always use 32-bit ops to zero upper register parts, even with 16-bit data
  • Avoid mixing register sizes in loops
  • For perf-critical code, check compiler's assembly output
  • Note divider throughput exceeds latency, but only without dependencies
  • On modern CPUs (Ice Lake and later), check uops.info specs for current metrics

This experiment shows how deep microarchitecture knowledge uncovers non-obvious optimizations. Swapping one seemingly pointless letter in an instruction yields threefold speedup by removing artificial pipeline limits. For systems programmers, it's a reminder: even in low-level assembly, optimization opportunities hide in hardware understanding.

β€” Editorial Team

Advertisement 728x90

Read Next