Al menos con una CPU de escritorio típica, realmente no se puede especificar mucho sobre el uso de la memoria caché. Sin embargo, aún puedes intentar escribir un código de caché. Por el lado del código, esto a menudo significa que los bucles de desenrollado (solo por un ejemplo obvio) raramente son útiles: expande el código, y una CPU moderna normalmente minimiza la sobrecarga de los bucles. En general, puede hacer más por el lado de los datos, para mejorar la localidad de referencia, proteger contra el uso compartido falso (por ejemplo, dos datos de uso frecuente que intentarán usar la misma parte del caché, mientras que otras partes no se utilizarán).
Editar (para hacer algunos puntos un poco más explícitos):
Una CPU típico tiene un número de diferentes cachés. Un procesador de escritorio moderno generalmente tendrá al menos 2 y a menudo 3 niveles de caché. Por (al menos casi) acuerdo universal, "nivel 1" es el caché "más cercano" a los elementos de procesamiento, y los números suben desde allí (nivel 2 es siguiente, nivel 3 después de eso, etc.)
In la mayoría de los casos, (al menos) el caché de nivel 1 se divide en dos mitades: un caché de instrucciones y un caché de datos (el Intel 486 es casi la única excepción de la que tengo conocimiento, con un único caché tanto para instrucciones como para datos). -pero es tan completamente obsoleto que probablemente no merezca una gran cantidad de pensamiento).
En la mayoría de los casos, un caché está organizado como un conjunto de "líneas". El contenido de un caché normalmente se lee, escribe y rastrea una línea a la vez. En otras palabras, si la CPU va a usar datos de cualquier parte de una línea de caché, toda esa línea de caché se lee desde el siguiente nivel de almacenamiento más bajo. Las cachés que están más cerca de la CPU generalmente son más pequeñas y tienen líneas de caché más pequeñas.
Esta arquitectura básica conduce a la mayoría de las características de un caché que importan al escribir código. En la medida de lo posible, desea leer algo en el caché una vez, hacer todo lo que esté a mano y luego pasar a otra cosa.
Esto significa que a medida que procesa datos, generalmente es mejor leer una cantidad relativamente pequeña de datos (lo suficientemente pequeños como para caber en la memoria caché), hacer todo el procesamiento de esos datos como sea posible, luego pasar a el siguiente fragmento de datos. Algoritmos como Quicksort que rápidamente rompen grandes cantidades de entrada en piezas progresivamente más pequeñas hacen esto de forma más o menos automática, por lo que tienden a ser bastante compatibles con el caché, casi independientemente de los detalles precisos de la memoria caché.
Esto también tiene implicaciones para la forma de escribir el código. Si usted tiene un bucle como:
for i = 0 to whatever
step1(data);
step2(data);
step3(data);
end for
Usted es generalmente mejor encadenar ya que muchos de los pasos juntos como se puede hasta la cantidad que quepa en la memoria caché. En el momento en que desborda el caché, el rendimiento puede/disminuirá drásticamente.Si el código del paso 3 anterior era lo suficientemente grande que no caben en la memoria caché, tendría por lo general será mejor interrumpir el circuito en dos piezas como esto (si es posible):
for i = 0 to whatever
step1(data);
step2(data);
end for
for i = 0 to whatever
step3(data);
end for
Loop desenrollar es un tema bastante disputado. Por un lado, puede conducir a un código que es mucho más amigable con la CPU, reduciendo la sobrecarga de las instrucciones ejecutadas para el bucle en sí. Al mismo tiempo, puede (y generalmente lo hace) aumentar el tamaño del código, por lo que es relativamente poco amigable con el caché. Mi propia experiencia es que en los puntos de referencia sintéticos que tienden a hacer cantidades realmente pequeñas de procesamiento en cantidades de datos realmente grandes, se gana mucho con el despliegue de bucles. En un código más práctico en el que tiende a tener más procesamiento en una pieza individual de datos, gana mucho menos, y el desbordamiento de la memoria caché que conduce a una pérdida de rendimiento grave no es particularmente raro en absoluto.
La memoria caché de datos también tiene un tamaño limitado. Esto significa que generalmente desea que sus datos estén empaquetados de la manera más densa posible, de modo que la mayor cantidad posible de datos encajará en la caché. Solo por un ejemplo obvio, una estructura de datos que está vinculada junto con punteros necesita ganar bastante en términos de complejidad computacional para compensar la cantidad de espacio de caché de datos utilizado por esos punteros. Si va a utilizar una estructura de datos vinculada, generalmente quiere al menos asegurarse de que está vinculando datos relativamente grandes.
En muchos casos, sin embargo, he encontrado que originalmente trucos que aprendí de ajuste de datos en una minúscula cantidad de memoria en diminutos procesadores que han sido (en su mayoría) obsoleto desde hace décadas, funciona bastante bien en los procesadores modernos. La intención ahora es incluir más datos en la memoria caché en lugar de la memoria principal, pero el efecto es casi el mismo. En algunos casos, puede pensar que las instrucciones de la CPU son casi gratuitas, y la velocidad de ejecución general se rige por el ancho de banda de la memoria caché (o la memoria principal), por lo que el procesamiento extra para descomprimir datos de un formato denso se resuelve en su favor. Esto es particularmente cierto cuando se trata de datos suficientes que ya no encajarán en la memoria caché, por lo que la velocidad general se rige por el ancho de banda de la memoria principal. En este caso, puede ejecutar un lote de instrucciones para guardar algunas lecturas de memoria y seguir adelante.
El procesamiento en paralelo puede agravar ese problema. En muchos casos, la reescritura del código para permitir el procesamiento en paralelo puede llevar virtualmente a ninguna ganancia en el rendimiento, o incluso a una pérdida de rendimiento. Si la velocidad total se rige por el ancho de banda de la CPU a la memoria, es improbable que tener más núcleos compitiendo por ese ancho de banda sirva para nada (y puede causar un daño sustancial). En tal caso, el uso de múltiples núcleos para mejorar la velocidad a menudo se reduce a hacer aún más para empaquetar los datos con mayor certeza y aprovechar aún más la potencia de procesamiento para descomprimir los datos, por lo que la ganancia de velocidad real es reducir el ancho de banda consumido , y los núcleos extra solo evitan perder tiempo para desempacar los datos del formato más denso.
Otro problema basado en caché que puede surgir en la codificación paralela es compartir (y compartir falsamente) las variables. Si dos (o más) núcleos necesitan escribir en la misma ubicación en la memoria, la línea de caché que contiene esos datos puede terminar yendo y viniendo entre los núcleos para dar a cada núcleo acceso a los datos compartidos. El resultado a menudo es un código que corre más lento en paralelo que en serie (es decir, en un solo núcleo). Hay una variación de esto llamada "uso compartido falso", en la cual el código en los diferentes núcleos está escribiendo en datos separados, pero los datos para los diferentes núcleos terminan en la misma línea de caché. Dado que el caché controla los datos puramente en términos de líneas enteras de datos, los datos se mezclan entre los núcleos de todos modos, lo que lleva exactamente al mismo problema.
"Lo suficientemente pequeño" es importante, pero también lo es "Lo suficientemente cerca" y "Lo suficientemente cerca juntos en el tiempo". Los cachés solo pueden contener tanto, así que conviértalo en un buen paquete apretado donde todo lo que necesitas AL MISMO TIEMPO sea físicamente adyacente en el mismo momento. – RocketRoy