En informática , el uso compartido falso es un patrón de uso que degrada el rendimiento y que puede surgir en sistemas con cachés distribuidos y coherentes 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 datos que sí 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é no es consciente de 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.
El uso más común de este término es en los cachés de CPU multiprocesador modernos , donde la memoria se almacena en caché en líneas de una pequeña potencia de dos palabras de tamaño (por ejemplo, 64 bytes alineados y contiguos ). Si dos procesadores operan con datos independientes en la misma región de dirección de memoria almacenable en una sola línea, los mecanismos de coherencia de caché en el sistema pueden forzar a toda la línea a través del bus o interconectarse con cada escritura de datos, lo que obliga a bloqueos de memoria además de desperdiciar ancho de banda del sistema . En algunos casos, la eliminación del uso compartido falso puede dar como resultado mejoras de rendimiento de orden de magnitud. [2] El uso compartido falso es un artefacto inherente de 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 <hilo> #include <nuevo> #include <atómico> #include <crono> #include <pestillo> #include <vector> utilizando el espacio de nombres std ; utilizando el espacio de nombres chrono ; #ifdefined(__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_constructive_interference_size ; #else // tamaño de línea de caché más común en caso contrario constexpr size_t CL_SIZE = 64 ; #endif int main () { vector < jthread > threads ; 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 nivel del núcleo latch coarseSync ( nThreads ); // sincronización fina mediante atomic en el espacio de usuario atomic_uint fineSync ( nThreads ); // tantos caracteres como quepan en una línea de caché struct alignas ( CL_SIZE ) { char shareds [ CL_SIZE ]; } cacheLine ; // suma de los tiempos de ejecución de todos los subprocesos atomic_int64_t nsSum ( 0 ); for ( int t = 0 ; t != nThreads ; ++ t ) subprocesos . emplace_back ( [ & ]( char volátil & c ) { coarseSync . arrive_and_wait (); // sincronizar el inicio de la ejecución del hilo en el nivel del núcleo if ( fineSync . fetch_sub ( 1 , memory_order :: relajado ) != 1 ) // sincronización fina en el nivel del usuario while ( fineSync . load ( memory_order :: relajado ) ); inicio automático = reloj_de_alta_resolución :: ahora (); for ( tamaño_t r = 10'000'000 ; r -- ; ) c = c + 1 ; nsSum += duración_cast < nanosegundos > ( reloj_de_alta_resolución :: now () - start ) .count ( ); }, ref ( cacheLine.shareds [ t ] ) ) ; threads.resize ( 0 ); // unir todos los hilos cout << nThreads << " : " << ( int ) ( nsSum /(1.0e7 * nThreads ) + 0.5 ) << endl ; } }
Este código muestra el efecto de la compartición falsa. Crea un número creciente de subprocesos desde un subproceso hasta el número 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 subprocesos:
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 tomar hasta 100 nanosegundos completar una operación de incremento en la línea de caché compartida, lo que corresponde a aproximadamente 420 ciclos de reloj en esta CPU.
Existen formas de mitigar los efectos de la compartición falsa. Por ejemplo, la compartición falsa en cachés de CPU se puede prevenir 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 la compartición falsa. [3] Sin embargo, algunas de estas transformaciones pueden no estar siempre permitidas. Por ejemplo, el borrador estándar del lenguaje de programación C++ de C++23 exige que los miembros de datos se distribuyan de manera que los miembros posteriores tengan direcciones más altas. [4]
Existen herramientas para detectar el uso compartido falso. [5] [6] También existen sistemas que detectan y reparan el uso compartido falso en la ejecución de programas. Sin embargo, estos sistemas generan cierta sobrecarga de ejecución. [7] [8]