Las reglas básicas son en realidad bastante simples. Donde se pone difícil es en cómo se aplican a su código.
El caché funciona en dos principios: localidad temporal y localidad espacial. La primera es la idea de que si recientemente utilizó una determinada porción de datos, probablemente la necesitará nuevamente pronto. Esto último significa que si recientemente usó los datos en la dirección X, probablemente pronto necesitará la dirección X + 1.
El caché intenta acomodar esto recordando los fragmentos de datos utilizados más recientemente. Funciona con líneas de caché, generalmente de un tamaño de 128 bytes, por lo que incluso si solo necesita un solo byte, toda la línea de caché que lo contiene se extrae en la caché. Entonces, si necesita el siguiente byte después, ya estará en el caché.
Y esto significa que siempre querrá que su propio código explote estas dos formas de localidad tanto como sea posible. No saltes sobre la memoria. Haga todo el trabajo que pueda en un área pequeña, y luego pase a la siguiente, y haga todo el trabajo que pueda.
Un ejemplo simple es el recorrido de la matriz 2D que mostró la respuesta de 1800. Si lo atraviesa una fila a la vez, está leyendo la memoria secuencialmente. Si lo hace en forma de columna, leerá una entrada, luego saltará a una ubicación completamente diferente (el comienzo de la siguiente fila), leerá una entrada y saltará nuevamente. Y cuando finalmente regrese a la primera fila, ya no estará en el caché.
Lo mismo se aplica al código. Los saltos o ramas significan un uso de caché menos eficiente (porque no estás leyendo las instrucciones secuencialmente, sino saltando a una dirección diferente). Por supuesto, las sentencias if pequeñas probablemente no cambiarán nada (solo omite unos pocos bytes, por lo que aún terminará dentro de la región en caché), pero las llamadas a funciones generalmente implican que está saltando a una completamente diferente dirección que no se puede almacenar en caché. A menos que se haya llamado recientemente.
Sin embargo, el uso de la caché de instrucciones suele ser mucho menos problemático. De lo que generalmente debe preocuparse es del caché de datos.
En una estructura o clase, todos los miembros se presentan de forma contigua, lo cual es bueno. En una matriz, todas las entradas también se presentan de forma contigua. En las listas vinculadas, cada nodo se asigna a una ubicación completamente diferente, lo cual es malo. Los punteros en general tienden a apuntar a direcciones no relacionadas, lo que probablemente resultará en una pérdida de caché si la desreferencia.
Y si desea explotar múltiples núcleos, puede ser realmente interesante, ya que, por lo general, solo una CPU puede tener una dirección determinada en su caché L1 a la vez. Entonces, si ambos núcleos acceden constantemente a la misma dirección, se producirán errores constantes en la memoria caché, ya que están peleando por la dirección.