Les règles de base sont en fait assez simples. Là où cela devient délicat, c'est dans la façon dont ils s'appliquent à votre code.
La cache fonctionne sur deux principes: la localité temporelle et la localité spatiale. Le premier est l'idée que si vous avez récemment utilisé un certain morceau de données, vous en aurez probablement besoin à nouveau bientôt. Ce dernier signifie que si vous avez récemment utilisé les données à l'adresse X, vous aurez probablement bientôt besoin de l'adresse X + 1.
Le cache essaie d'accommoder cela en se souvenant des morceaux de données les plus récemment utilisés. Il fonctionne avec des lignes de cache, généralement d'une taille d'environ 128 octets, donc même si vous n'avez besoin que d'un seul octet, toute la ligne de cache qui le contient est tirée dans le cache. Donc, si vous avez besoin de l'octet suivant par la suite, il sera déjà dans le cache.
Et cela signifie que vous voudrez toujours que votre propre code exploite autant que possible ces deux formes de localité. Ne sautez pas partout dans la mémoire. Faites autant de travail que possible sur une petite zone, puis passez à la suivante, et faites autant de travail que possible.
Un exemple simple est le parcours de tableau 2D que la réponse de 1800 a montré. Si vous le parcourez une ligne à la fois, vous lisez la mémoire de manière séquentielle. Si vous le faites par colonne, vous lirez une entrée, puis sauterez à un emplacement complètement différent (le début de la ligne suivante), lirez une entrée et sauterez à nouveau. Et lorsque vous reviendrez enfin à la première ligne, elle ne sera plus dans le cache.
La même chose s'applique au code. Les sauts ou les branches signifient une utilisation moins efficace du cache (car vous ne lisez pas les instructions séquentiellement, mais sautez vers une adresse différente). Bien sûr, de petites instructions if ne changeront probablement rien (vous ne sautez que quelques octets, vous vous retrouverez donc toujours dans la région mise en cache), mais les appels de fonction impliquent généralement que vous passez à un tout autre adresse qui ne peut pas être mise en cache. À moins qu'il n'ait été appelé récemment.
Cependant, l'utilisation du cache d'instructions est généralement beaucoup moins problématique. Ce dont vous devez généralement vous soucier, c'est le cache de données.
Dans une structure ou une classe, tous les membres sont disposés de manière contiguë, ce qui est bien. Dans un tableau, toutes les entrées sont également disposées de manière contiguë. Dans les listes chaînées, chaque nœud est alloué à un emplacement complètement différent, ce qui est mauvais. Les pointeurs en général ont tendance à pointer vers des adresses non liées, ce qui entraînera probablement un échec du cache si vous le déréférencer.
Et si vous souhaitez exploiter plusieurs cœurs, cela peut devenir vraiment intéressant, car généralement, un seul processeur peut avoir une adresse donnée dans son cache L1 à la fois. Donc, si les deux cœurs accèdent constamment à la même adresse, il en résultera des échecs constants dans le cache, car ils se disputent l'adresse.