We’re Officially Live β€” Lifetime Founding Membership Available for a Limited Time
Free preview

Arrays & Memory Layout

Contiguous storage is why arrays give O(1) indexing and why row-major traversal is cache-friendly, plus array-of-structs vs struct-of-arrays for embedded data.

22 min read

In this lesson:

This builds on the C arrays lesson; here the focus is layout and its performance consequences.


Contiguous storage is the whole trick

An array stores its elements back-to-back in memory. That single property is why a[i] is O(1): the address is pure arithmetic, no searching.

address of a[i] = base + i * sizeof(element)
 a[0]  a[1]  a[2]  a[3]
β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”
β”‚ 10 β”‚ 20 β”‚ 30 β”‚ 40 β”‚   each cell sizeof(int) apart
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”˜
base  +4   +8   +12

The CPU computes the address in one step and reads it. No other structure gives O(1) random access for free, that's the array's superpower.


Array operation costs

Operation Cost Why
Index a[i] O(1) address arithmetic
Update a[i] = x O(1) direct write
Search (unsorted) O(n) must scan
Search (sorted) O(log n) binary search
Insert/delete at end O(1) (if capacity remains)
Insert/delete at front/middle O(n) must shift the rest

That last row is the array's weakness: inserting at the front means shifting every later element up one slot. When you need cheap middle-insertion, a linked list (next lessons) trades away O(1) indexing to get it.


2D arrays are 1D underneath: row-major order

C stores a 2D array row by row, all of row 0, then all of row 1 (this is row-major order). The compiler maps m[r][c] to a flat offset:

offset(r, c) = r * COLS + c
int m[2][3]:   m[0][0] m[0][1] m[0][2] m[1][0] m[1][1] m[1][2]
 in memory β†’  β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”
              β”‚  a   β”‚  b   β”‚  c   β”‚  d   β”‚  e   β”‚  f   β”‚
              β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜
                └──── row 0 β”€β”€β”€β”€β”˜ └──── row 1 β”€β”€β”€β”€β”˜

This matters when you manage a flat buffer yourself (common in embedded, a framebuffer, a sample matrix): you index it as buf[r * cols + c].


Traversal order and cache locality

Two loops that are both O(nΒ·m) can run very differently because of the cache, a small fast memory that loads a line (e.g. 32 bytes) around each access. Walking memory sequentially uses every byte of each loaded line; jumping around wastes most of it.

// FAST: row-major traversal matches memory order β€” sequential, cache-friendly
for (int r = 0; r < ROWS; r++)
    for (int c = 0; c < COLS; c++)
        sum += m[r][c];        // consecutive addresses
 
// SLOW: column-major traversal jumps COLS elements each step β€” cache-hostile
for (int c = 0; c < COLS; c++)
    for (int r = 0; r < ROWS; r++)
        sum += m[r][c];        // strided, thrashes the cache

On a Cortex-M7 or any cached core, the first can be several times faster despite identical Big-O. Match your traversal to the storage order. (On a small Cortex-M0 with no cache the effect is smaller, but sequential access still helps flash wait-states and prefetch.)


Array-of-structs vs struct-of-arrays

How you group fields changes locality. Say you log samples with a timestamp and three axes:

// Array of Structs (AoS): natural, fields of one sample together
struct Sample { uint32_t t; int16_t x, y, z; };
struct Sample log_aos[256];
 
// Struct of Arrays (SoA): each field in its own array
struct {
    uint32_t t[256];
    int16_t  x[256], y[256], z[256];
} log_soa;

For a DSP filter that processes one axis at a time, SoA can be markedly faster; for "handle one event fully," AoS is simpler. Pick based on your dominant access pattern.


Gotchas


TL;DR

This is just the start

Sign up free to track progress on this lesson, get coding problems with the Run button, and unlock the full interview Q&A.

More in Data Structures & Algorithms

Time & Space ComplexityRing Buffers
Linked Lists
Stacks & Queues
Hash Tables
Static vs Dynamic Allocation