Strides explain why views are free, why some reshapes copy, and why loop order changes speed by 10x.
Every ndarray is a flat buffer of bytes plus metadata. The strides tuple says how many bytes to jump to move one step along each axis. For a C-ordered (3, 4) array of float64, strides are (32, 8): the next column is 8 bytes away, the next row 32 bytes (4 elements x 8).
The address of a[i, j] is simply offset + i*strides[0] + j*strides[1]. That one formula explains a lot:
- Slicing with a step just multiplies a stride:
a[:, ::2]has strides(32, 16)over the same buffer. - Transposing swaps the strides,
(8, 32), again without moving data. - Broadcasting uses a stride of 0, so one value is reused along an axis.
- Reshape can only return a view when the new shape can be expressed with strides over the existing layout; otherwise it must copy.
C order (row-major, NumPy's default) keeps the last axis contiguous; Fortran order (order="F") keeps the first axis contiguous. a.flags shows C_CONTIGUOUS and F_CONTIGUOUS. np.ascontiguousarray returns a C-contiguous copy if needed, which some C libraries and GPU transfers require.
Layout matters for speed because CPUs fetch memory in 64-byte cache lines. Reductions or loops that walk along the contiguous axis read sequential memory; walking across it jumps by large strides and wastes most of each cache line. Summing a large C-ordered matrix along axis=1 versus over its transpose can differ noticeably for that reason.
np.lib.stride_tricks.sliding_window_view(x, w) uses overlapping strides to give all windows of length w without copying. Its low-level cousin as_strided can build any view, including ones that read outside the buffer, so avoid it unless you have checked the arithmetic.
Example
import numpy as np
a = np.arange(12, dtype=np.float64).reshape(3, 4)
print(a.strides) # (32, 8)
print(a[:, ::2].strides) # (32, 16) same buffer
print(a.T.strides) # (8, 32) axes swapped
print(a.T.flags["C_CONTIGUOUS"], a.T.flags["F_CONTIGUOUS"]) # False True
f = np.asfortranarray(a)
print(f.strides) # (8, 24)
b = np.broadcast_to(np.array([1.0, 2.0]), (1000, 2))
print(b.strides) # (0, 8) no copy, stride 0
from numpy.lib.stride_tricks import sliding_window_view
x = np.arange(6)
w = sliding_window_view(x, 3) # all length-3 windows, a view
print(w) # [[0 1 2] [1 2 3] [2 3 4] [3 4 5]]
print(w.strides, np.shares_memory(w, x)) # (8, 8) True
print(w.mean(axis=1)) # moving average [1. 2. 3. 4.]Remember
- Strides are bytes per step along each axis; indexing is offset + sum(i*stride)
- Slices, transposes and broadcasts are new strides over the same buffer
- C order: last axis contiguous; F order: first axis contiguous
- Iterate and reduce along the contiguous axis for cache efficiency
Watch out for
- Writing through a
sliding_window_vieworas_stridedview: overlapping elements alias the same memory (the former is read-only by default for that reason).