En informática , el intercambio falso es un patrón de uso que degrada el rendimiento y que puede surgir en sistemas con cachés coherentes y distribuidos del tamaño del bloque de recursos más pequeño administrado por el mecanismo de almacenamiento en caché. Cuando un participante del sistema intenta acceder periódicamente a datos que no están siendo alterados por otra parte, pero esos datos comparten un bloque de caché con los datos que están siendo alterados, el protocolo de almacenamiento en caché puede obligar al primer participante a recargar todo el bloque de caché a pesar de la falta de necesidad lógica. [1] El sistema de almacenamiento en caché desconoce la actividad dentro de este bloque y obliga al primer participante a soportar la sobrecarga del sistema de almacenamiento en caché requerida por el verdadero acceso compartido de un recurso.
Con diferencia, el uso más común de este término es en las cachés de CPU de multiprocesadores modernos , donde la memoria se almacena en caché en líneas de una potencia pequeña de dos palabras (por ejemplo, 64 bytes contiguos alineados ). Si dos procesadores operan con datos independientes en la misma región de direcciones de memoria almacenables en una sola línea, los mecanismos de coherencia de la caché en el sistema pueden forzar toda la línea a través del bus o interconectarse con cada escritura de datos, lo que forzará paradas de memoria además de desperdiciar el ancho de banda del sistema. . En algunos casos, la eliminación del intercambio falso puede dar lugar a mejoras de rendimiento de gran magnitud. [2] El uso compartido falso es un artefacto inherente a los protocolos de caché sincronizados automáticamente y también puede existir en entornos como sistemas de archivos distribuidos o bases de datos, pero la prevalencia actual se limita a los cachés de RAM.
#include <iostream> #include <thread> #include <new> #include <atomic> #include <chrono> #include <latch> #include <vector> usando el espacio de nombres estándar ; usando el espacio de nombres crono ; #if definido (__cpp_lib_hardware_interference_size) // tamaño de línea de caché predeterminado desde el tiempo de ejecución constexpr size_t CL_SIZE = hardware_constructivo_interference_size ; #else // tamaño de línea de caché más común; de lo contrario, constexpr size_t CL_SIZE = 64 ; #terminara si int main () { vector < jthread > hilos ; int hc = jthread :: hardware_concurrency (); hc = hc <= CL_SIZE ? hc : CL_SIZE ; for ( int nThreads = 1 ; nThreads <= hc ; ++ nThreads ) { // sincronizar el comienzo de los subprocesos de forma aproximada en el pestillo del nivel del kernel coarseSync ( nThreads ); // sincronización fina mediante atomic en el espacio de usuario atomic_uint fineSync ( nThreads ); // tantos caracteres como cabrían en una línea de caché struct alignas ( CL_SIZE ) { charshares [ CL_SIZE ] ; } línea de caché ; // suma de los tiempos de ejecución de todos los subprocesos atomic_int64_t nsSum ( 0 ); para ( int t = 0 ; t ! = nThreads ; ++ t ) subprocesos . emplace_back ( [ & ]( char volatile & c ) { coarseSync . Arrival_and_wait (); // sincroniza el comienzo de la ejecución del hilo a nivel de kernel if ( fineSync . fetch_sub ( 1 , Memory_order :: Relaxed ) != 1 ) // fine- sincronizar a nivel de usuario mientras ( fineSync . cargar ( memoria_orden :: relajado ) ); inicio automático = reloj_alta_resolución :: ahora (); para ( tamaño_t r = 10'000'000 ; r - ; ) c = c + 1 ; nsSum += duración_cast < nanosegundos > ( reloj_alta_resolución :: ahora () - inicio ). contar (); }, ref ( cacheLine . compartidos [ t ] ) ); hilos . cambiar el tamaño ( 0 ); // unir todos los hilos cout << nThreads << ": " << ( int )( nsSum / ( 1.0e7 * nThreads ) + 0.5 ) << endl ; } }
Este código muestra el efecto del intercambio falso. Crea una cantidad cada vez mayor de subprocesos desde un subproceso hasta la cantidad de subprocesos físicos en el sistema. Cada subproceso incrementa secuencialmente un byte de una línea de caché, que en su conjunto se comparte entre todos los subprocesos. Cuanto mayor sea el nivel de contención entre subprocesos, más tiempo llevará cada incremento. Estos son los resultados en un sistema Zen4 con 16 núcleos y 32 hilos:
1: 12: 43: 64: 95: 116: 137: 158: 179: 1610: 1811: 2112: 2513: 2914: 3515: 3916: 4117: 4318:4419: 4820: 4921: 5122: 5323: 5824: 6125:6826: 7527: 7928: 8229: 8530: 8831: 9132: 94
Como puede ver, en el sistema en cuestión puede tardar hasta 100 nanosegundos en completar una operación de incremento en la línea de caché compartida, lo que corresponde a aprox. 420 ciclos de reloj en esta CPU.
Hay formas de mitigar los efectos del intercambio falso. Por ejemplo, el intercambio falso en las cachés de la CPU se puede evitar reordenando las variables o agregando relleno (bytes no utilizados) entre las variables. Sin embargo, algunos de estos cambios de programa pueden aumentar el tamaño de los objetos, lo que lleva a un mayor uso de memoria. [2] Las transformaciones de datos en tiempo de compilación también pueden mitigar el intercambio falso. [3] Sin embargo, es posible que algunas de estas transformaciones no siempre estén permitidas. Por ejemplo, el borrador estándar del lenguaje de programación C++ de C++ 23 exige que los miembros de datos deben distribuirse de manera que los miembros posteriores tengan direcciones más altas. [4]
Existen herramientas para detectar intercambios falsos. [5] [6] También existen sistemas que detectan y reparan el intercambio falso en programas en ejecución. Sin embargo, estos sistemas incurren en algunos gastos generales de ejecución. [7] [8]