Cómo acelerar su código usando cachés de CPU

La memoria caché de la CPU reduce la latencia de la memoria cuando se accede a los datos desde la memoria principal del sistema. Los desarrolladores pueden y deben aprovechar la memoria caché de la CPU para mejorar el rendimiento de las aplicaciones.

Cómo funcionan las cachés de la CPU

Las CPU modernas suelen tener tres niveles de caché, etiquetados como L1, L2 y L3, que reflejan el orden en que la CPU los comprueba. Las CPU suelen tener una caché de datos, una caché de instrucciones (para el código) y una caché unificada (para cualquier cosa). Acceder a estos cachés es mucho más rápido que acceder a la RAM: por lo general, el caché L1 es aproximadamente 100 veces más rápido que la RAM para el acceso a los datos, y el caché L2 es 25 veces más rápido que la RAM para el acceso a los datos.

Cuando su software se ejecuta y necesita extraer datos o instrucciones, primero se comprueban las cachés de la CPU, luego la RAM más lenta del sistema y finalmente las unidades de disco mucho más lentas. Es por eso que desea optimizar su código para buscar primero lo que probablemente se necesite en la memoria caché de la CPU.

Su código no puede especificar dónde residen las instrucciones y los datos (el hardware de la computadora hace eso), por lo que no puede forzar ciertos elementos en la memoria caché de la CPU. Pero puede optimizar su código para recuperar el tamaño de la caché L1, L2 o L3 en su sistema utilizando Windows Management Instrumentation (WMI) para optimizar cuando su aplicación accede a la caché y por lo tanto su rendimiento.

Las CPU nunca acceden al caché byte a byte. En su lugar, leen la memoria en líneas de caché, que son fragmentos de memoria que generalmente tienen un tamaño de 32, 64 o 128 bytes.

La siguiente lista de códigos ilustra cómo puede recuperar el tamaño de caché de CPU L2 o L3 en su sistema:

public static uint GetCPUCacheSize (string cacheType) {try {using (ManagementObject managementObject = new ManagementObject ("Win32_Processor.DeviceID = 'CPU0'")) {return (uint) (managementObject [cacheType]); }} captura {retorno 0; }} static void Main (string [] args) {uint L2CacheSize = GetCPUCacheSize ("L2CacheSize"); uint L3CacheSize = GetCPUCacheSize ("L3CacheSize"); Console.WriteLine ("L2CacheSize:" + L2CacheSize.ToString ()); Console.WriteLine ("L3CacheSize:" + L3CacheSize.ToString ()); Console.Read (); }

Microsoft tiene documentación adicional sobre la clase WMI Win32_Processor.

Programación para rendimiento: código de ejemplo

Cuando tiene objetos en la pila, no hay gastos generales de recolección de basura. Si está utilizando objetos basados ​​en el montón, siempre hay un costo relacionado con la recolección de basura generacional para recolectar o mover objetos en el montón o compactar la memoria del montón. Una buena forma de evitar la sobrecarga de la recolección de basura es usar estructuras en lugar de clases.

Los cachés funcionan mejor si usa una estructura de datos secuencial, como una matriz. El orden secuencial permite que la CPU pueda leer con anticipación y también leer con anticipación especulativamente en anticipación de lo que probablemente se solicite a continuación. Por tanto, un algoritmo que accede a la memoria de forma secuencial siempre es rápido.

Si accede a la memoria en un orden aleatorio, la CPU necesita nuevas líneas de caché cada vez que accede a la memoria. Eso reduce el rendimiento.

El siguiente fragmento de código implementa un programa simple que ilustra los beneficios de usar una estructura sobre una clase:

 struct RectangleStruct {public int amplitud; altura pública int; } class RectangleClass {public int amplitud; altura pública int; }

El siguiente código describe el rendimiento de usar una matriz de estructuras contra una matriz de clases. Con fines ilustrativos, he usado un millón de objetos para ambos, pero normalmente no necesitas tantos objetos en tu aplicación.

static void Main (string [] args) {constante int tamaño = 1000000; var structs = new RectangleStruct [tamaño]; clases var = new RectangleClass [tamaño]; var sw = nuevo cronómetro (); sw.Start (); para (var i = 0; i <tamaño; ++ i) {structs [i] = new RectangleStruct (); estructuras [i]. ancho = 0 estructuras [i]. altura = 0; } var structTime = sw.ElapsedMilliseconds; sw.Reset (); sw.Start (); para (var i = 0; i <tamaño; ++ i) {clases [i] = new RectangleClass (); clases [i] .breadth = 0; clases [i] .altura = 0; } var classTime = sw.ElapsedMilliseconds; sw.Stop (); Console.WriteLine ("Tiempo que tarda la matriz de clases:" + classTime.ToString () + "milisegundos."); Console.WriteLine ("Tiempo que tarda la matriz de estructuras:" + structTime.ToString () + "milisegundos."); Console.Read (); }

El programa es simple: crea 1 millón de objetos de estructuras y los almacena en una matriz. También crea 1 millón de objetos de una clase y los almacena en otra matriz. A la anchura y la altura de las propiedades se les asigna un valor de cero en cada instancia.

Como puede ver, el uso de estructuras compatibles con caché proporciona una gran ganancia de rendimiento.

Reglas generales para un mejor uso de la memoria caché de la CPU

Entonces, ¿cómo se escribe el código que mejor utiliza el caché de la CPU? Desafortunadamente, no existe una fórmula mágica. Pero hay algunas reglas generales:

  • Evite el uso de algoritmos y estructuras de datos que presenten patrones irregulares de acceso a la memoria; utilice estructuras de datos lineales en su lugar.
  • Utilice tipos de datos más pequeños y organice los datos para que no haya agujeros de alineación.
  • Considere los patrones de acceso y aproveche las estructuras de datos lineales.
  • Mejore la localidad espacial, que utiliza cada línea de caché al máximo una vez que se ha asignado a un caché.