Zurück zur Startseite

Arrays und Cache: O(1)-Zugriffsoptimierung

Das Kapitel über Arrays zeigt, wie Cache-Lokalität die echte O(1)-Zugriffsgeschwindigkeit bestimmt. Analyse von Strides, mehrdimensionalen Arrays, Matrix-Multiplikation und AoS/SoA mit Benchmarks der Beschleunigung bis zu 10×.

Arrays töten den Cache: Wie man 10 Mal schneller wird
Advertisement 728x90

Arrays optimieren, um Cache-Misses zu minimieren

Sequentieller Zugriff auf Array-Elemente ist 7–10-mal schneller als zufälliger Zugriff, da der Cache-Speicher so funktioniert. Eine einzelne 64-Byte-Cache-Zeile lädt 16 sequentielle ints, was Misses minimiert, wenn das richtige Traversierungs-Muster verwendet wird. Profiling mit perf stat deckt das Problem auf: 450k Cache-Misses pro 1M Anweisungen deuten auf suboptimalen Array-Einsatz hin, sogar bei einfachen Paketverarbeitungsaufgaben.

Auswirkungen des Zugriffs-Strides auf die Leistung

Der Zugriffs-Stride bestimmt die Effizienz beim Laden von Cache-Zeilen. Bei Stride 1 werden alle 64 Bytes voll ausgenutzt, und der Prefetcher erkennt das Muster, um das Laden zu beschleunigen.

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

Solch eine Sequenz benötigt 107 Takte für 8 Elemente (13,4 Takte/Element). Zufälliger Zugriff über Indizes erhöht die Kosten auf 800 Takte (100 Takte/Element).

Google AdInline article slot

Benchmark auf einem Array mit 1M Elementen:

  • Stride 1: 1,2 ms (100 % Zeilenausnutzung)
  • Stride 2: 1,3 ms (50 % Ausnutzung)
  • Stride 4: 1,5 ms
  • Stride 8: 2,1 ms
  • Stride 16: 3,8 ms (6,25 % Ausnutzung)
  • Stride 64: 8,5 ms

Empfehlung: Stride ≤8 Elemente für akzeptable Leistung. Das Tool lmbench lat_mem_rd bestätigt: Kleine Strides (128–512 Bytes) halten Daten in L1 (3–4 ns), große (64 KB) verlagern sie in den DRAM (100+ ns).

Mehrdimensionale Arrays: Traversierungsreihenfolge ist entscheidend

C verwendet row-major-Reihenfolge: Elemente einer Zeile liegen kontinuierlich im Speicher. Traversierung nach Zeilen gewährleistet sequentiellen Zugriff, nach Spalten führt zu einem Stride von 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}
};

Bei einer 1024×1024-Matrix dauert die Zeilentour 12 ms, die Spaltentour 45 ms (3,75× langsamer).

Optimierung der Matrixmultiplikation

Naive ijk-Reihenfolge:

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] wird spaltenweise gelesen (Stride N=1024, 4096 Bytes). Die ikj-Reihenfolge behebt das:

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];
        }
    }
}

Ergebnisse für 512×512: ijk — 2450 ms, ikj — 680 ms (3,6× Beschleunigung).

Bei großen Matrizen — Tiling mit 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];
                    }
                }
            }
        }
    }
}

Blöcke passen in L1, mit Datenwiederverwendung. Für 1024×1024: 1800 ms (10× schneller als naiv).

AoS vs. SoA: Datenorganisation

Array of Structures (AoS) gruppiert Teilchenfelder, aber eine Cache-Zeile enthält ungenutzte Daten (37,5 % Ausnutzung).

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

Structure of Arrays (SoA) trennt nach Typ:

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

Positionsaktualisierungen in SoA nutzen Cache-Zeilen zu 100 % aus. Benchmark bei 1M Teilchen, 1000 Iterationen: AoS — 2850 ms, SoA — 1200 ms (2,4× Beschleunigung).

Wichtige Erkenntnisse

  • Sequentieller Array-Zugriff minimiert Cache-Misses um das 7+ fache im Vergleich zu zufälligem
  • Zugriffs-Stride ≤8 Elemente gewährleistet >90 % Cache-Zeilenausnutzung
  • Row-major-Reihenfolge in C erfordert Zeilentour; Spaltenzugriff verlangsamt um das 3–4 fache
  • Änderung der Schleifenreihenfolge bei Matrixoperationen bringt 3–4× Beschleunigung
  • SoA übertrifft AoS bei SIMD-Verarbeitung und häufigem Zugriff auf Feldsubsets

— Editorial Team

Advertisement 728x90

Weiterlesen