Memory Contiguity¶
Memory layout affects whether operations return views or copies.
Mental Model
C-contiguous means elements in the last axis are adjacent in memory (row-major), while Fortran-contiguous means elements in the first axis are adjacent (column-major). Operations that traverse memory in order are fast; operations that jump around cause cache misses and slow down. Transpose flips contiguity without moving data.
Why Contiguity Matters for Performance
Contiguity determines whether an operation can return a view (O(1), no copy) or must create a copy (O(n), new allocation). For example, reshape on a C-contiguous array is free (just change shape/strides), but on a non-contiguous array it forces a copy. Contiguous memory also enables CPU cache-line prefetching, making element-wise operations significantly faster. When performance matters, check a.flags['C_CONTIGUOUS'] and call np.ascontiguousarray(a) if needed.
C Contiguous¶
C-style row-major memory layout.
1. Row-Major Order¶
``` Array: [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
Memory: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] ```
Elements are stored row by row.
2. NumPy Default¶
NumPy arrays are C-contiguous by default.
Fortran Contiguous¶
Fortran-style column-major memory layout.
1. Column-Major Order¶
``` Array: [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
Memory: [0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11] ```
Elements are stored column by column.
2. Transpose Effect¶
Transposing a C-contiguous array makes it Fortran-contiguous.
View Chain Example¶
Track memory sharing through operations.
1. Initial Array¶
```python import numpy as np
def main(): x = np.arange(12) print(f"{id(x) = }")
if name == "main": main() ```
2. Reshape (View)¶
```python import numpy as np
def main(): x = np.arange(12) print(f"{id(x) = }")
y = x.reshape(3, 4) # view
print(f"{id(y) = }")
y[-1, -1] = -11
print(f"{x = }")
if name == "main": main() ```
x and y share the same memory block.
3. Transpose (View)¶
```python import numpy as np
def main(): x = np.arange(12) print(f"{id(x) = }")
y = x.reshape(3, 4) # view
print(f"{id(y) = }")
z = y.T # still a view
print(f"{id(z) = }")
z[-1, -1] = -11
print(f"{x = }")
if name == "main": main() ```
z still shares memory but interprets it column-wise.
Forced Copy¶
Non-contiguous arrays force copies when reshaped.
1. Copy Scenario¶
```python import numpy as np
def main(): x = np.arange(12) print(f"{id(x) = }")
y = x.reshape(3, 4) # view
print(f"{id(y) = }")
z = y.T # view (Fortran contiguous)
print(f"{id(z) = }")
w = z.reshape((-1,)) # COPY (must reorder data)
print(f"{id(w) = }")
w[-1] = -11
print(f"{x = }")
print(f"{y = }")
print(f"{z = }")
print(f"{w = }")
if name == "main": main() ```
2. Why Copy Needed¶
z is Fortran-contiguous [0,4,8,1,5,9,2,6,10,3,7,11] in memory.
Flattening to C-order requires reordering, forcing a copy.
3. Memory Independence¶
w has different id() and modifications don't affect x, y, z.
Checking Contiguity¶
Inspect array memory layout flags.
1. Flags Attribute¶
```python import numpy as np
x = np.arange(12).reshape(3, 4) print(x.flags) ```
Output:
C_CONTIGUOUS : True
F_CONTIGUOUS : False
...
2. After Transpose¶
```python import numpy as np
x = np.arange(12).reshape(3, 4) y = x.T print(y.flags) ```
Output:
C_CONTIGUOUS : False
F_CONTIGUOUS : True
...
Best Practices¶
Guidelines for working with memory layout.
1. Be Aware¶
Know when operations return views vs copies.
2. Use id()¶
Track object identity to verify memory sharing.
3. Explicit Copy¶
When data integrity is critical, call .copy() explicitly.
4. Check flags¶
Use .flags to inspect contiguity when debugging.
Exercises¶
Exercise 1.
Create a C-contiguous array a = np.arange(12).reshape(3, 4) and a Fortran-contiguous array b = np.asfortranarray(a). Check flags['C_CONTIGUOUS'] and flags['F_CONTIGUOUS'] for both. Print the strides of each and explain the difference.
Solution to Exercise 1
import numpy as np
a = np.arange(12).reshape(3, 4)
b = np.asfortranarray(a)
print(f"a C_CONTIGUOUS: {a.flags['C_CONTIGUOUS']}") # True
print(f"a F_CONTIGUOUS: {a.flags['F_CONTIGUOUS']}") # False
print(f"a strides: {a.strides}") # (32, 8)
print(f"b C_CONTIGUOUS: {b.flags['C_CONTIGUOUS']}") # False
print(f"b F_CONTIGUOUS: {b.flags['F_CONTIGUOUS']}") # True
print(f"b strides: {b.strides}") # (8, 24)
Exercise 2.
Starting from a = np.arange(20).reshape(4, 5), create a slice b = a[:, ::2]. Check whether b is C-contiguous or F-contiguous. Explain why slicing with a step can break contiguity.
Solution to Exercise 2
import numpy as np
a = np.arange(20).reshape(4, 5)
b = a[:, ::2]
print(f"b C_CONTIGUOUS: {b.flags['C_CONTIGUOUS']}") # False
print(f"b F_CONTIGUOUS: {b.flags['F_CONTIGUOUS']}") # False
print(f"b strides: {b.strides}")
# Step slicing skips elements, so the stride on axis 1
# is doubled, breaking the contiguous memory pattern.
Exercise 3.
Use np.ascontiguousarray to convert a non-contiguous slice back to a C-contiguous array. Verify the conversion by checking the flags before and after. Measure the memory addresses to confirm a copy was made.
Solution to Exercise 3
import numpy as np
a = np.arange(20).reshape(4, 5)
b = a[:, ::2] # non-contiguous
c = np.ascontiguousarray(b)
print(f"Before: C_CONTIGUOUS={b.flags['C_CONTIGUOUS']}")
print(f"After: C_CONTIGUOUS={c.flags['C_CONTIGUOUS']}")
print(f"Copy made: {c.base is not b}")