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)
| Dimension | Symbol | Size |
|---|---|---|
| Outermost (highest) | D₃ | 2 |
| Middle | D₂ | 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:
| Element | Row-major offset | Column-major offset |
|---|---|---|
array[0][0][2] | 2 | 8 |
array[1][1][2] | 11 | 11 |
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:
- Applies the correct formula (row-major or column-major) for its storage convention
- Multiplies the offset by
element_size - Reads
element_sizebytes from the resulting address - 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