Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Example of Column Major Order

Now that you understand how column-major order works conceptually, let’s make it concrete with the same 3D array we used for the row-major example — so you can see exactly how the memory layout differs.


The Array: 2 × 2 × 3 (Same as Before)

DimensionSymbolSize
Outermost (highest)D₃2
MiddleD₂2
Innermost (lowest)D₁3

The logical shape hasn’t changed — 2 layers, each a 2×3 grid. Only the way it’s serialised into memory is different.

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart TB
  subgraph L0["Layer 0  ── D₃ = 0"]
    direction LR
    subgraph R00["D₂ = 0"]
      direction LR
      a000["[0][0][0]"] --- a001["[0][0][1]"] --- a002["[0][0][2]"]
    end
    subgraph R01["D₂ = 1"]
      direction LR
      a010["[0][1][0]"] --- a011["[0][1][1]"] --- a012["[0][1][2]"]
    end
    R00 ~~~ R01
  end
  subgraph L1["Layer 1  ── D₃ = 1"]
    direction LR
    subgraph R10["D₂ = 0"]
      direction LR
      a100["[1][0][0]"] --- a101["[1][0][1]"] --- a102["[1][0][2]"]
    end
    subgraph R11["D₂ = 1"]
      direction LR
      a110["[1][1][0]"] --- a111["[1][1][1]"] --- a112["[1][1][2]"]
    end
    R10 ~~~ R11
  end
  L0 ~~~ L1

Logical representation of the 3D array (D₃=2, D₂=2, D₁=3) — 2 layers, each a 2×3 grid, 12 elements total. The logical shape is identical to the row-major example.


Layout in Memory

This is where column-major and row-major diverge completely.

Column-major rule:

The highest dimension (D₃) moves the fastest. The lowest dimension (D₁) moves the slowest.

Think of it as a counter where the leftmost digit ticks fastest. D₃ sprints through 0→1, then D₂ increments, D₃ resets and sprints again. D₁ only changes when both D₂ and D₃ have completed full cycles.

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart LR
  subgraph S0["D₁ = 0  (slowest — changes last)"]
    direction LR
    subgraph S0R0["D₂ = 0"]
      a000["[0][0][0]"] -->|"D₃ ↑"| a100["[1][0][0]"]
    end
    subgraph S0R1["D₂ = 1"]
      a010["[0][1][0]"] -->|"D₃ ↑"| a110["[1][1][0]"]
    end
    a100 -->|"D₂ ↑"| a010
  end
  subgraph S1["D₁ = 1"]
    direction LR
    subgraph S1R0["D₂ = 0"]
      a001["[0][0][1]"] -->|"D₃ ↑"| a101["[1][0][1]"]
    end
    subgraph S1R1["D₂ = 1"]
      a011["[0][1][1]"] -->|"D₃ ↑"| a111["[1][1][1]"]
    end
    a101 -->|"D₂ ↑"| a011
  end
  subgraph S2["D₁ = 2  (slowest — changes last)"]
    direction LR
    subgraph S2R0["D₂ = 0"]
      a002["[0][0][2]"] -->|"D₃ ↑"| a102["[1][0][2]"]
    end
    subgraph S2R1["D₂ = 1"]
      a012["[0][1][2]"] -->|"D₃ ↑"| a112["[1][1][2]"]
    end
    a102 -->|"D₂ ↑"| a012
  end
  a110 -->|"D₁ ↑"| a001
  a111 -->|"D₁ ↑"| a002

The highest dimension D₃ moves the fastest in column-major order — D₃ races 0→1 at every step; D₂ only increments after D₃ overflows; D₁ (lowest) only increments last of all.

The full serialisation order for our 2×2×3 array:

[0][0][0] → [1][0][0] → [0][1][0] → [1][1][0] →
[0][0][1] → [1][0][1] → [0][1][1] → [1][1][1] →
[0][0][2] → [1][0][2] → [0][1][2] → [1][1][2]

Compare this to row-major (where D₁ moved fastest):

[0][0][0] → [0][0][1] → [0][0][2] → [0][1][0] → ... ← row-major
[0][0][0] → [1][0][0] → [0][1][0] → [1][1][0] → ... ← column-major

The same 12 elements — entirely different order.


Structure in Memory

With base_address = 2 and element_size = 4 bytes (integers), here is the column-major physical layout:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
block-beta
  columns 6
  A["[0][0][0]<br/>addr 2"]
  B["[1][0][0]<br/>addr 6"]
  C["[0][1][0]<br/>addr 10"]
  D["[1][1][0]<br/>addr 14"]
  E["[0][0][1]<br/>addr 18"]
  F["[1][0][1]<br/>addr 22"]
  G["[0][1][1]<br/>addr 26"]
  H["[1][1][1]<br/>addr 30"]
  I["[0][0][2]<br/>addr 34"]
  J["[1][0][2]<br/>addr 38"]
  K["[0][1][2]<br/>addr 42"]
  L["[1][1][2]<br/>addr 46"]

Structure of the 3D array stored in column-major order in memory — base address 2, element size 4 bytes. Notice D₃ alternates 0↔1 in every adjacent pair of slots.

Notice how adjacent slots in memory always differ only in their D₃ index (0 or 1). That’s the signature of D₃ moving fastest.

# Verify the column-major memory layout
base = 2
element_size = 4
D3, D2, D1 = 2, 2, 3

print(f"{'Index':<16} {'Offset':>6} {'Address':>8}")
print("-" * 32)

# Column-major: D1 slowest, D2 medium, D3 fastest
for i1 in range(D1):           # D1 — slowest outer loop
    for i2 in range(D2):       # D2 — middle
        for i3 in range(D3):   # D3 — fastest inner loop
            offset  = i1 * (D2 * D3) + i2 * D3 + i3
            address = base + offset * element_size
            print(f"[{i3}][{i2}][{i1}]          {offset:>6}    {address:>6}")

Calculating the Address of Elements

For an N-dimensional array in column-major order, the offset formula is the mirror image of row-major:

offset  = I₁ × (D₂ × D₃ × ... × Dₙ)
        + I₂ × (D₃ × ... × Dₙ)
        + ...
        + Iₙ₋₁ × Dₙ
        + Iₙ

address = base + offset × element_size

For our 3D array (Dₙ = D₃, D₂, D₁):

offset = I₁ × (D₂ × D₃)  +  I₂ × D₃  +  I₃

Let’s compute both elements from the problem statement:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart LR
  subgraph Calc1["array[0][0][2]  —  I₃=0, I₂=0, I₁=2"]
    direction TB
    C1A["D₁ contribution<br/>I₁ × (D₂×D₃) = 2 × (2×2) = 8"]
    C1B["D₂ contribution<br/>I₂ × D₃ = 0 × 2 = 0"]
    C1C["D₃ contribution<br/>I₃ = 0"]
    C1D["offset = 8 + 0 + 0 = 8"]
    C1E["address = 2 + (8 × 4) = 34 ✓"]
    C1A --> C1B --> C1C --> C1D --> C1E
  end
  subgraph Calc2["array[1][1][2]  —  I₃=1, I₂=1, I₁=2"]
    direction TB
    C2A["D₁ contribution<br/>I₁ × (D₂×D₃) = 2 × (2×2) = 8"]
    C2B["D₂ contribution<br/>I₂ × D₃ = 1 × 2 = 2"]
    C2C["D₃ contribution<br/>I₃ = 1"]
    C2D["offset = 8 + 2 + 1 = 11"]
    C2E["address = 2 + (11 × 4) = 46 ✓"]
    C2A --> C2B --> C2C --> C2D --> C2E
  end

Calculating the base address for array[0][0][2] (offset 8, address 34) and array[1][1][2] (offset 11, address 46) using the column-major subscript operator formula.

Cross-check against the memory layout: array[0][0][2] is at position 8 → address 34 ✓. array[1][1][2] is the last element at position 11 → address 46 ✓.

Now compare these offsets with the row-major results from the previous chapter:

ElementRow-major offsetColumn-major offset
array[0][0][2]28
array[1][1][2]1111

array[1][1][2] lands at offset 11 in both orderings — because it’s the last element regardless of how you count. array[0][0][2], however, is at offset 2 in row-major (early) but offset 8 in column-major (late). The storage order completely reshuffles the positions.

# Compare row-major vs column-major offsets for the same elements
D3, D2, D1 = 2, 2, 3

def row_major_offset(i3, i2, i1):
    return i3 * (D2 * D1) + i2 * D1 + i1

def col_major_offset(i3, i2, i1):
    return i1 * (D2 * D3) + i2 * D3 + i3

for elem in [(0, 0, 2), (1, 1, 2)]:
    i3, i2, i1 = elem
    rm = row_major_offset(i3, i2, i1)
    cm = col_major_offset(i3, i2, i1)
    print(f"array[{i3}][{i2}][{i1}]: row-major offset={rm}, col-major offset={cm}")

Dereferencing the Value

Once the address is resolved, the final step — dereferencing — is identical in both orderings. The language reads element_size bytes from the resolved address and interprets them as the stored type.

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart LR
  Addr["Resolved address<br/>e.g. 34  (for array[0][0][2])"]
  Read["Read 4 consecutive bytes<br/>starting at address 34"]
  Interp["Interpret bytes<br/>according to stored datatype<br/>(int → 4 bytes → one integer)"]
  Val["Return value to caller ✓"]

  Addr --> Read --> Interp --> Val

Dereferencing is storage-order agnostic — once the address is known, the language reads element_size bytes from that point and interprets them as the declared type, regardless of whether the array is row-major or column-major.

The subscript operator hides all of this arithmetic. When you write array[i3][i2][i1], the language silently:

  1. Applies the correct formula (row-major or column-major) for its storage convention
  2. Multiplies the offset by element_size
  3. Reads element_size bytes from the resulting address
  4. Returns the interpreted value

As a programmer, you write array[i][j] the same way for both orderings — but the bytes your CPU reads are in completely different positions.


Key Takeaways

  • Column-major serialises the same 3D array in a completely different order to row-major
  • D₃ (highest) moves fastest; D₁ (lowest) moves slowest — the exact reverse of row-major
  • The 3D column-major formula: base + (I₁ × D₂ × D₃ + I₂ × D₃ + I₃) × element_size
  • Dereferencing (reading the value) is identical regardless of storage order — only the address differs
  • Mixing row-major and column-major conventions silently produces wrong values — never wrong addresses