DEV Community

Cover image for The Pointer Arithmetic of Constant Time: Why Arrays Scale O(1)
Doogal Simpson
Doogal Simpson

Posted on • Originally published at doogal.dev

The Pointer Arithmetic of Constant Time: Why Arrays Scale O(1)

TL;DR: Array indexing is O(1) because it relies on a single pointer calculation: base_address + (index * element_size). Since arrays use contiguous memory allocation, the CPU uses the base register and an offset to resolve any memory address in a fixed number of cycles, regardless of the array's total length.

We treat array indexing as a fundamental constant in software engineering. Whether you are pulling an item from a list of ten or ten million, the performance remains the same. This isn't a result of clever searching algorithms; it is a side effect of how memory is physically structured and addressed at the hardware level. When you access arr[i], you aren't searching; you are calculating a physical location on the memory bus.

How does a computer find an element in an array?

The CPU resolves an index by adding a calculated offset to the array's base memory address. This is a single-step arithmetic operation: BaseAddress + (Index * ElementSize).

At the machine level, when you declare an array, the operating system allocates a contiguous block of memory. This means there are no gaps between elements. The variable name arr is effectively a pointer to the very first byte of that block (the base address). Because the memory is unbroken, the CPU doesn't need to traverse the data. It uses the index as a multiplier. If you need the element at index 500, the CPU doesn't look at the first 499. It performs one multiplication, one addition, and then issues a LOAD instruction for that specific memory address. This direct access is why we call it "constant time."

Why must array elements be the same size?

Fixed element sizes ensure that the memory offset for any given index is predictable and can be calculated in a single CPU cycle. Without uniform sizing, the CPU would be unable to jump directly to a specific index and would instead have to scan every preceding byte to find where the next element starts.

In lower-level languages like C or Rust, this is why arrays are typed. An array of int64_t tells the compiler that every element is exactly 8 bytes wide. If you mixed data types of different sizes, the formula Index * Size would break. Even in high-level languages like JavaScript or Python where lists appear to hold mixed types, the engine is actually storing an array of pointers. Since every pointer on a 64-bit system is 8 bytes, the underlying memory math remains O(1) even if the objects those pointers reference are of varying sizes.

Feature Array (Contiguous) Linked List (Non-Contiguous)
Access Time O(1) - Constant O(n) - Linear
Memory Layout Sequential blocks Scattered nodes with pointers
Address Resolution Base + (i * size) Iterative traversal
Cache Efficiency High (Spatial Locality) Low (Pointer Chasing)

Why is Spatial Locality important for array performance?

Beyond simple math, arrays are fast because they take advantage of the CPU cache through spatial locality. When the CPU fetches a single element from an array, it doesn't just grab those specific bytes; it pulls an entire cache line (typically 64 bytes) into the L1 cache.

Because arrays are contiguous, the elements surrounding your target are likely already sitting in the cache when you need them. This hardware-level optimization is why iterating through an array is significantly faster than iterating through a linked list. In a linked list, each node might be located in a completely different page of memory, forcing the CPU to stall while it waits for a fetch from main RAM. This is often called "pointer chasing," and it is the primary bottleneck in non-contiguous data structures.

// A direct look at pointer arithmetic
int64_t* arr = (int64_t*)malloc(1000 * sizeof(int64_t));
int64_t index = 500;

// Under the hood, arr[index] is equivalent to:
// 1. Take the base address (arr)
// 2. Add (500 * 8 bytes)
// 3. Dereference the resulting address
int64_t value = *(arr + index);
Enter fullscreen mode Exit fullscreen mode

How does the instruction set handle indexing?

Most modern CPUs have specific instruction set architectures (ISA) designed to make this math even faster. For example, x86-64 has a "scaled index" addressing mode. It can perform the multiplication of the index by the element size (if the size is 2, 4, or 8 bytes) and the addition to the base pointer as part of a single instruction. This means the overhead for accessing the 10,000th element is literally zero compared to accessing the 1st element.

FAQ

What happens if I access an index out of bounds?
In languages like C, the CPU will perform the math, jump to that memory address, and attempt to read it. This usually results in a segmentation fault if the memory belongs to another process, or a buffer over-read where you pull "garbage" data into your application. It is a logic error, not a performance hit.

Do dynamic arrays (like Python Lists) still have O(1) access?
Yes. While dynamic arrays can resize themselves by allocating a new, larger contiguous block and copying the old data over, the indexing math remains Base + (Index * Size). The resizing operation is O(n), but it happens infrequently enough that the "amortized" cost of operations remains constant.

Why are arrays zero-indexed?
Zero-indexing is a direct reflection of the pointer math. The index represents the offset from the base address. The first element is located exactly at the base address, meaning it has an offset of zero (Base + 0 * Size). If arrays were 1-indexed, the CPU would have to subtract 1 from the index every time you accessed an element, adding unnecessary cycles to every single lookup.

Top comments (0)