Back to Home

Arrays and cache: O(1) access optimization

The chapter on arrays shows how cache locality determines the real O(1) access speed. Analysis of strides, multidimensional arrays, matrix multiplication and AoS/SoA with benchmarks of acceleration up to 10×.

Arrays kill cache: how to speed up 10 times
Advertisement 728x90

# Optimizing Arrays to Minimize Cache Misses

Sequential access to array elements is 7–10 times faster than random access due to how cache memory works. A single 64-byte cache line loads 16 sequential ints, which minimizes misses when using the right traversal pattern. Profiling with perf stat uncovers the problem: 450k cache-misses per 1M instructions points to suboptimal array usage even in simple packet processing tasks.

Impact of Access Stride on Performance

Access stride determines cache line loading efficiency. With stride 1, all 64 bytes are fully utilized, and the prefetcher recognizes the pattern to speed up fetching.

int sum = 0;
for (int i = 0; i < 8; i++) {
    sum += array[i];
}

Such a sequence takes 107 cycles for 8 elements (13.4 cycles/element). Random access via indices increases costs to 800 cycles (100 cycles/element).

Google AdInline article slot

Benchmark on a 1M element array:

  • Stride 1: 1.2 ms (100% line utilization)
  • Stride 2: 1.3 ms (50% utilization)
  • Stride 4: 1.5 ms
  • Stride 8: 2.1 ms
  • Stride 16: 3.8 ms (6.25% utilization)
  • Stride 64: 8.5 ms

Recommendation: stride ≤8 elements for acceptable performance. The lmbench lat_mem_rd tool confirms: small strides (128–512 bytes) keep data in L1 (3–4 ns), large ones (64 KB) push to DRAM (100+ ns).

Multidimensional Arrays: Traversal Order is Critical

C uses row-major order: elements in a row are contiguous in memory. Traversing by rows ensures sequential access, while by columns introduces a stride of 16+ bytes.

Google AdInline article slot
int matrix[4][4] = {
    {0, 1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10, 11},
    {12, 13, 14, 15}
};

For a 1024×1024 matrix, row traversal takes 12 ms, column traversal 45 ms (3.75× slower).

Optimizing Matrix Multiplication

Naive ijk order:

for (int i = 0; i < N; i++) {
    for (int j = 0; j < N; j++) {
        for (int k = 0; k < N; k++) {
            C[i][j] += A[i][k] * B[k][j];
        }
    }
}

Problem: B[k][j] is read column-wise (stride N=1024, 4096 bytes). The ikj order fixes it:

Google AdInline article slot
for (int i = 0; i < N; i++) {
    for (int k = 0; k < N; k++) {
        int r = A[i][k];
        for (int j = 0; j < N; j++) {
            C[i][j] += r * B[k][j];
        }
    }
}

Results for 512×512: ijk — 2450 ms, ikj — 680 ms (3.6× speedup).

For large matrices — tiling with BLOCK_SIZE=64:

for (int ii = 0; ii < N; ii += BLOCK_SIZE) {
    for (int jj = 0; jj < N; jj += BLOCK_SIZE) {
        for (int kk = 0; kk < N; kk += BLOCK_SIZE) {
            for (int i = ii; i < min(ii + BLOCK_SIZE, N); i++) {
                for (int k = kk; k < min(kk + BLOCK_SIZE, N); k++) {
                    int r = A[i][k];
                    for (int j = jj; j < min(jj + BLOCK_SIZE, N); j++) {
                        C[i][j] += r * B[k][j];
                    }
                }
            }
        }
    }
}

Blocks fit in L1, with data reuse. For 1024×1024: 1800 ms (10× faster than naive).

AoS vs SoA: Data Organization

Array of Structures (AoS) groups particle fields, but a cache line contains unused data (37.5% utilization).

typedef struct {
    float x, y, z;
    float vx, vy, vz;
    float mass;
    int id;
} particle_t;

Structure of Arrays (SoA) separates by type:

typedef struct {
    float x[1000], y[1000], z[1000];
    float vx[1000], vy[1000], vz[1000];
    float mass[1000];
    int id[1000];
} particles_t;

Position updates in SoA utilize cache lines at 100%. Benchmark on 1M particles, 1000 iterations: AoS — 2850 ms, SoA — 1200 ms (2.4× speedup).

Key Takeaways

  • Sequential array access minimizes cache misses 7+ times compared to random
  • Access stride ≤8 elements ensures >90% cache line utilization
  • Row-major order in C requires row traversal; column access slows things down 3–4 times
  • Changing loop order in matrix operations yields 3–4× speedup
  • SoA outperforms AoS with SIMD processing and frequent access to field subsets

— Editorial Team

Advertisement 728x90

Read Next