Retour à l'accueil

Écart O(1) vs O(log n) : Cache dans les Structures de Données

L’article analyse pourquoi la table de hachage O(1) cède face à la recherche binaire O(log n) en raison de 71 % de misses de cache contre 21 %. Les benchmarks montrent array 2,5 fois plus rapide que liste chaînée. Aperçu de la hiérarchie mémoire et des lignes de cache pour les développeurs middle/senior.

Table de hachage vs recherche binaire : pourquoi le cache bat la théorie
Advertisement 728x90

## Pourquoi les algorithmes O(1) perdent face à O(log n) : Cache et performances réelles

Durant le développement d'un bootloader pour un SoC RISC-V, nous avons rencontré un problème : la recherche de configurations de périphériques dans une table de 500 éléments prenait trop de temps. Une table de hachage avec une complexité amortie O(1) entraînait des temps de démarrage supérieurs à 100 ms — trois ordres de grandeur pires. Passer à une recherche binaire sur un tableau trié en O(log n) a accéléré les choses de 40 %.

Le profilage avec perf a révélé la différence de comportement du cache :

# Table de hachage
$ perf stat -e cache-references,cache-misses ./bootloader_hash
  1,247,832 cache-references
    892,441 cache-misses (71.5% miss rate)

# Recherche binaire
$ perf stat -e cache-references,cache-misses ./bootloader_binsearch
    423,156 cache-references
     89,234 cache-misses (21.1% miss rate)

Les misses de cache dans la table de hachage atteignaient 71,5 %, chacune coûtant ~100 cycles. La recherche binaire a réduit les misses à 21,1 % grâce à une meilleure localité d'accès.

Google AdInline article slot

Tableau vs Liste chaînée : Une expérience

La somme de 100 000 entiers a révélé un écart énorme malgré les deux en O(n) :

Tableau : 70 147 ns (17 557 410 cycles)
Liste chaînée : 179 169 ns (44 740 656 cycles)
Le tableau est 2,55 fois plus rapide

Le code du tableau utilise un accès séquentiel :

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

Liste chaînée :

Google AdInline article slot
typedef struct node {
    int value;
    struct node *next;
} node_t;

node_t *head = NULL;
for (int i = 0; i < 100000; i++) {
    node_t *node = malloc(sizeof(node_t));
    node->value = i;
    node->next = head;
    head = node;
}

long long sum = 0;
node_t *curr = head;
while (curr) {
    sum += curr->value;
    curr = curr->next;
}

Le tableau l'emporte grâce à la localité spatiale : une ligne de cache (64 bytes) charge 16 éléments d'un coup. La liste chaînée provoque ~70 % de misses à cause des pointeurs dispersés.

Hiérarchie mémoire et latences

Les systèmes réels ont une hiérarchie à plusieurs niveaux :

| Niveau | Taille | Latence | Relatif aux registres |

Google AdInline article slot

|-------------|-------------|----------------|------------------------|

| Registres | ~256 B | 1 cycle | 1× |

| L1 | 32-64 KB | 3-4 cycles | 3× |

| L2 | 256 KB-1 MB | 12-15 cycles | 12× |

| L3 | 4-32 MB | 40-50 cycles | 40× |

| DRAM | GB | 100-200 cycles | 100× |

Un seul miss DRAM équivaut à 100 opérations ALU. Dans RISC-V embarqué :

  • L1 : 16-32 KB (contre 64 KB sur les ordinateurs de bureau)
  • Pas de L3
  • DRAM à 100 MHz

Un jeu de données >16 KB signifie des misses constants.

Lignes de cache et localité

Les CPU chargent des lignes de 64 bytes. L'accès séquentiel excelle :

  • Tableau : 94 % de hits
  • Accès aléatoire : misses fréquents
  • Nœud de liste (16 B) : 75 % de la ligne gaspillée

Le préchargement matériel aide les motifs séquentiels mais échoue sur les pointeurs.

Enseignements principaux

  • Le cache domine : les misses coûtent 100 fois plus que les ops ALU
  • La localité règne : O(n) avec bon cache > O(log n) avec mauvais cache
  • L'embarqué est plus dur : de petits caches exigent de rentrer en L1
  • Toujours profiler : perf révèle les vrais goulots d'étranglement
  • Les algorithmes évoluent : les manuels vieillissent sur silicium

— Editorial Team

Advertisement 728x90

Lire ensuite