Reading x86-64 Array, String, and Structure Loops
Trace pointer-length contracts, strides, sentinels, and bounds checks in real loops
Reading x86-64 Array, String, and Structure Loops
These blog posts are written as lessons. Examples use Intel syntax. Pointer validity, object lifetime, aliasing, and overflow remain contracts at the C/C++ boundary.
Many real loops walk from a starting address at a regular stride while testing or accumulating values. Arrays, strings, and arrays of structures all lower to this pattern, but differ in termination and stride.
Find four pieces of loop state
- A base or current pointer
- An index, end pointer, or remaining count
- An accumulator or search result
- A terminating condition and its signedness
; rcx = int32_t* values, rdx = count
xor eax, eax
xor r8d, r8d
loop_test:
cmp r8, rdx
jae loop_done
add eax, dword ptr [rcx + r8*4]
inc r8
jmp loop_test
loop_done:
ret
rcx is the base, r8 the index, eax the accumulator, and unsigned r8 >= rdx ends the loop.
Index and end-pointer loops can express the same range
lea rdx, [rdi + rsi*4] ; end = begin + count
xor eax, eax
cmp rdi, rdx
je done
loop_begin:
add eax, dword ptr [rdi]
add rdi, 4
cmp rdi, rdx
jne loop_begin
done:
ret
There is no explicit index. Recover the range from the initial pointer, four-byte increment, and computed end. Machine addition can wrap, but valid C/C++ pointer arithmetic has a narrower object-range contract.
Strings terminate by length or sentinel
A byte span with an explicit length behaves like an array. A C string uses a zero byte as a sentinel.
strlen_like:
mov rax, rcx
scan:
cmp byte ptr [rax], 0
je found
inc rax
jmp scan
found:
sub rax, rcx
ret
This code receives no maximum length. It assumes that a NUL terminator exists inside readable memory. Without one, it continues beyond the object.
Do not mix byte count, UTF-8 code units, Unicode code points, and user-perceived grapheme clusters. A strlen-style scan counts bytes, not characters.
Search-result representation needs a contract
mov rax, -1
xor r9d, r9d
loop_test:
cmp r9, rdx
jae not_found
cmp byte ptr [rcx + r9], r8b
je found
inc r9
jmp loop_test
found:
mov rax, r9
not_found:
ret
If the return type is size_t, bitwise -1 is SIZE_MAX, much like string_view::npos. Caller and callee must not disagree about signed -1 versus an unsigned maximum.
Structures combine stride and field offset
struct Particle {
float x, y, z;
uint32_t live;
};
; rcx = Particle*, rdx = count
xorps xmm0, xmm0
test rdx, rdx
jz done
loop_begin:
cmp dword ptr [rcx + 12], 0
je skip
addss xmm0, dword ptr [rcx]
skip:
add rcx, 16
dec rdx
jne loop_begin
done:
ret
Offset 12 suggests live, offset 0 suggests x, and the 16-byte step suggests sizeof(Particle). These are observations about this binary layout, not permanent source names. Verify them with compiler layout information and static_assert.
AoS and SoA produce different address patterns
Array of Structures repeats a stride and several fixed offsets. Structure of Arrays tends to use separate base pointers for each field. SoA can make one-field bulk processing contiguous and SIMD-friendly; AoS can be better when all fields of an object are consumed together. Data layout can matter more than a clever instruction.
Bounds-check elimination is a proof
for (size_t i = 0; i < n; ++i)
out[i] = in[i] * 2;
The loop condition dominates each access, so there may be no additional per-element check. However, potential overlap between out and in can block reordering or vectorization. Do not silently assume non-overlap in handwritten assembly.
Recognize unrolling and vector tails
loop4:
add eax, [rcx]
add eax, [rcx + 4]
add eax, [rcx + 8]
add eax, [rcx + 12]
add rcx, 16
sub rdx, 4
jae loop4
One machine iteration handles four source elements. A remainder path usually handles zero to three. Do not infer that length must be a multiple of four until that tail is examined.
A vectorized loop often has a prologue, SIMD main loop, and scalar tail:
movdqu xmm0, [rcx]
paddd xmm1, xmm0 ; four 32-bit lanes
The XMM value is not necessarily one 128-bit integer; the opcode defines packed lane width.
Copy contracts differ
memcpyrequires non-overlapping source and destination.memmovemust work with overlap.- A typed loop adds object lifetime, alignment, aliasing, and type semantics.
Compilers may inline a fixed-size copy, call a runtime routine, or use rep movsb. No one form is universally fastest for every CPU and size distribution.
Size arithmetic is a security boundary
bytes = count * sizeof(Item);
If multiplication wraps to a small allocation but a later loop still uses count, a heap overflow can result. Audit allocation-size computation and loop bounds together. Validate count <= SIZE_MAX / sizeof(Item) or use checked multiplication.
Loop checklist
- What minimum valid range does each pointer require?
- Is the count measured in bytes or elements?
- What are the stride and field offsets?
- Does termination use index, end pointer, or sentinel?
- Does zero length skip the body?
- Are last-element and one-past-end handled correctly?
- Is there a remainder after unrolled or SIMD work?
- May source and destination overlap?
- Can size multiplication or pointer addition overflow?
Summary
Data loops decompose into pointer, stride, accumulator, and termination. Index and end-pointer forms can represent the same range; sentinel loops replace an explicit maximum with a strong terminator assumption. Recover the complete memory contract, including empty input, tails, overlap, units, and size overflow.