The Buffer Protocol & Zero-Copy Memory Views
Master high-performance binary memory sharing: the CPython Buffer Protocol, Py_buffer struct layout, N-dimensional stride math, and memoryview slicing.
Transferring large binary buffers (such as NumPy arrays, machine learning tensors, network packets, or audio streams) between Python and native C extensions often incurs devastating memory copy overhead. Slicing a standard 500 MB bytes object requires allocating a brand new 500 MB block in RAM and copying all 500,000,000 bytes over the memory bus!
To eliminate this bottleneck, CPython incorporates the Buffer Protocol: a standardized C-level interface that allows distinct objects to share underlying contiguous raw memory buffers with zero memory copies.
Slicing a memoryview adjusts pointer offsets and strides without copying a single byte.
1. The Anatomy of Py_buffer#
Any CPython object can export its internal memory buffer by implementing the tp_as_buffer slot. Exporting a buffer populates a standardized C descriptor struct: Py_buffer:
123456789101112typedef struct bufferinfo { void *buf; // Raw C memory pointer to the start of the buffer PyObject *obj; // Owning Python object (retains refcount lock) Py_ssize_t len; // Total buffer length in bytes Py_ssize_t itemsize; // Size of each element in bytes int readonly; // 1 if buffer is read-only, 0 if mutable int ndim; // Number of dimensions (1 for flat buffers) char *format; // Struct format string (e.g. 'B' for uchar, 'd' for double) Py_ssize_t *shape; // Array of dimension lengths Py_ssize_t *strides; // Array of byte steps to advance one index per dimension void *internal; // Allocator private data } Py_buffer;
2. Multi-Dimensional Stride Mathematics#
In multi-dimensional numerical computing (such as matrices and images), memory is laid out as a contiguous 1D byte array.
The Buffer Protocol maps -dimensional array coordinates to physical memory addresses using the strides vector :
Where each stride defines the exact number of bytes the pointer must jump in physical RAM to advance by one index along dimension .
3. High-Performance Benchmarks: bytes vs memoryview#
Compare the runtime and memory overhead of slicing a 100 MB buffer using standard string slicing versus memoryview:
1234567891011121314151617181920212223import time # Create a 100 MB binary buffer in RAM large_buffer = bytearray(b"x" * (100 * 1024 * 1024)) print("=== Standard Slicing: Copies 50 MB to new heap allocation ===") start = time.perf_counter() # Standard slicing copies all 50,000,000 bytes into a brand new object! copied_slice = bytes(large_buffer[: 50 * 1024 * 1024]) elapsed_copy = time.perf_counter() - start print(f"Standard slice copy time: {elapsed_copy * 1000.0:.3f} ms") print("\n=== Zero-Copy memoryview: Adjusts pointer offset in O(1) ===") start = time.perf_counter() # memoryview creates a lightweight view sharing the original memory! view = memoryview(large_buffer) zero_copy_slice = view[: 50 * 1024 * 1024] elapsed_view = time.perf_counter() - start print(f"Zero-copy slice time : {elapsed_view * 1000.0:.3f} ms") # Speedup factor speedup = elapsed_copy / max(elapsed_view, 1e-9) print(f"\nSpeedup: {speedup:.1f}x faster!")
4. In-Place Mutable Buffer Operations#
Because a memoryview over a bytearray points directly to the underlying physical memory, mutating the view immediately updates the original buffer:
12345678raw_data = bytearray(b"HELLO WORLD") view = memoryview(raw_data) # Mutate a slice through the view view[6:11] = b"PYTHON" # The underlying bytearray reflects the mutation immediately with zero copies! print(f"Updated raw data: {raw_data}") # b'HELLO PYTHON'
memoryview is active on a container like bytearray, the container is locked against resizing! Attempting to append(), extend(), or change the capacity of the bytearray raises BufferError: Existing exports of data: object cannot be re-sized.5. Concept Verification#
Test your understanding of the buffer protocol:
How does slicing a memoryview achieve O(1) time complexity regardless of the size of the underlying buffer?
What happens if you try to append data to a bytearray while an active memoryview is referencing it?