JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Loops, Arrays, and Pointers in Assembly

A loop is just a branch that aims backward, an array is just a base address plus arithmetic, and a pointer is just a number you load from. Watch these three friendly C ideas dissolve into a handful of register-and-branch instructions — and meet the address-math that makes cache-friendly code fast.

A loop is a branch pointed backward

In guide 1 we watched an if/else turn into nothing but a conditional branch — a test, then a jump that either falls through or skips ahead. A loop is built from exactly the same Lego brick, with one twist: the jump aims backward, to an instruction we have already run. Remember that the program counter normally just walks forward, one instruction after the next. A branch is the only thing that can pick it up and set it down somewhere else. Point it at an earlier address and the machine re-runs the same block of instructions — that, and nothing more mysterious, is a loop.

Think about how you'd write `for (i = 0; i < n; i++)` by hand. You need a register holding the counter i, a way to test `i < n` at the top, the body in the middle, an increment, and a branch at the bottom that jumps back to the test. Compilers usually shape it cleverly — test once at the top, branch backward at the bottom — so the only per-iteration cost is the body plus one increment plus one branch. The whole skeleton of every loop you have ever written collapses to that.

# C:  sum = 0;  for (i = 0; i < n; i++)  sum += a[i];
# x10 = base address of a[]   x11 = n   x12 = sum   x13 = i

      li    x12, 0           # sum = 0
      li    x13, 0           # i = 0
loop: bge   x13, x11, done   # if i >= n, leave the loop
      # ... body goes here (next section) ...
      addi  x13, x13, 1      # i++
      j     loop             # jump BACKWARD to the test
done: # ... sum (x12) is ready ...
A counted loop in RISC-V. The 'j loop' at the bottom is the backward jump; 'bge ... done' is the exit test that decides when to stop.

An array is a base address plus arithmetic

Now the body: `sum += a[i]`. In C, `a[i]` feels like a single magic step. The hardware has no idea what `a[i]` means — it only knows addresses. So the compiler must compute where a[i] lives. An array is laid out as a run of equal-sized elements packed one after another in memory, so the address of element i is simply `base + i x element_size`. If a[] holds 4-byte integers, the address of a[i] is `base + i x 4`. That multiply-then-add is the whole secret of array indexing; there is no array type down here, only this little sum.

Once the address is in a register, fetching the value is one load. Recall from the ISA rung that RISC-V is a load-store machine: arithmetic touches only registers, and the only way to reach memory is a load or a store. The base-plus-offset shape we use here — "address in this register, plus a small constant" — is one of the machine's few addressing modes, and it maps perfectly onto `base + i x 4`. So one multiply, one add, and one load turn the friendly `a[i]` into something the silicon can actually do.

  1. Scale the index to a byte offset. Multiply i by the element size: for 4-byte ints, that is i x 4. A compiler does this as a one-bit-per-power-of-two left shift (shift i left by 2 means times 4) because a shift is far cheaper than a general multiply.
  2. Add the base address. Take the array's base address (sitting in a register) and add that byte offset. Now one register holds the exact address of a[i] — which is precisely a pointer to that element.
  3. Load through it. Issue a single load using base-plus-offset addressing — "the address in this register, plus 0" — and the value of a[i] arrives in a register, ready for the arithmetic that follows.

Pointers: an address is just a number

Here is the quiet punchline of this whole guide: down at the metal, a pointer is nothing special. It is simply a register (or a memory slot) that happens to hold an address — a number that names a place in memory. Look back at the array code: `x14` held `base + i x 4`, which is exactly a pointer to a[i]. "Dereferencing a pointer" is just doing a load from the address it holds; `0(x14)` is `*p`. C draws a thick conceptual line between an integer and a pointer, but the hardware sees both as bit patterns in registers, and the same add instruction can advance either one.

This unlocks a neater way to walk an array. Instead of recomputing `base + i x 4` every iteration, keep a moving pointer and bump it by the element size each time. The loop holds an address, loads through it, then adds 4 to step to the next element. This is the assembly shadow of C's pointer arithmetic — `p++` on an int pointer secretly adds 4, not 1, because the compiler scales by the element size. The hardware does no scaling magic; the compiler bakes the 4 in for you. One honest caution: a pointer naming an address says nothing about whether that address is yours to touch — a stray pointer happily loads garbage or crashes the program.

Where loops meet the cache

Here the assembly we just wrote quietly touches the deepest performance idea in the whole subject. When that `lw` walks through a[0], a[1], a[2], ... in order, it is reading neighbouring addresses one after another. Memory hardware loves this. A cache — that small desk holding your most-used books so you rarely walk to the library — fetches memory a whole cache line at a time (say 64 bytes), not one int at a time. So a single trip to memory brings back a[i] and its sixteen neighbours. The next fifteen iterations then find their data already on the desk. This is spatial locality: using something makes nearby things cheap to use next.

Now flip it. Walk the same array with a large stride — touch a[0], then a[1000], then a[2000] — and every load lands on a fresh line the cache has not seen. Each one is a cache miss, a slow walk to the real library. The instructions are nearly identical; the addresses they generate are not. This is why cache-friendly code can be many times faster than cache-hostile code that computes the exact same answer. The arithmetic in your loop is usually free; it's the memory access pattern those addresses trace that decides your speed.

What's worth carrying forward (and an honest word)

Step back and the three ideas rhyme. A loop is a backward branch that rewrites the program counter. An array index is a multiply-and-add that builds an address. A pointer is just that address sitting in a register, dereferenced by a load. Branches, address arithmetic, and loads — the same small vocabulary from guide 1, now arranged to chew through whole collections of data. Everything richer (a hash table, a linked list, a tree) is built from exactly these moves, just with the addresses computed in fancier ways.

One honest note to close, the same one this rung keeps making: almost nobody hand-writes loops like these for production today — the compiler does it, and usually better than a human, because it can juggle registers and instruction scheduling at a scale we cannot. The reason to read this guide is not to out-code the compiler. It is so that when a profiler points at a hot loop, or a pointer bug crashes at a baffling address, or one version of your code is mysteriously ten times slower than another, you can read the assembly and see what the machine is really doing — the addresses, the loads, the memory pattern. Reading the machine's language matters far more now than writing it.