c++ - microsoft - visual studio installer
¿Las arquitecturas x86 actuales admiten cargas no temporales(desde la memoria "normal")? (2)
Para responder específicamente a la pregunta principal:
Sí , las últimas 1 CPU principales de Intel admiten cargas no temporales en la memoria 2 normal , pero solo "indirectamente" mediante instrucciones de movntdqa
no temporal, en lugar de utilizar directamente instrucciones de carga no temporales como movntdqa
. Esto está en contraste con las tiendas no temporales donde puede simplemente usar las instrucciones de la tienda no temporales correspondientes 3 directamente.
La idea básica es emitir una prefetchnta
a la línea de caché antes de cualquier carga normal, y luego emitir cargas de forma normal. Si la línea no estaba ya en la memoria caché, se cargará de manera no temporal. El significado exacto de la moda no temporal depende de la arquitectura, pero el patrón general es que la línea se carga en al menos el L1 y quizás algunos niveles de caché más altos. De hecho, para que una captación previa sea de alguna utilidad, debe hacer que la línea se cargue al menos en algún nivel de caché para que la consuma una carga posterior. La línea también se puede tratar especialmente en el caché, por ejemplo marcándola como de alta prioridad para el desalojo o restringiendo las formas en que se puede colocar.
El resultado de todo esto es que si bien las cargas no temporales son compatibles en cierto sentido, en realidad son solo parcialmente no temporales a diferencia de las tiendas en las que realmente no se deja rastro de la línea en ninguno de los niveles de caché. Las cargas no temporales causarán un poco de contaminación de la caché, pero generalmente menos que las cargas regulares. Los detalles exactos son específicos de la arquitectura, y he incluido algunos detalles a continuación para Intel moderno (puede encontrar un texto un poco más largo en esta respuesta ).
Cliente Skylake
Según las pruebas en esta respuesta , parece que el comportamiento del prefetchnta
Skylake es buscar normalmente en el caché L1, omitir el L2 por completo y obtener de forma limitada el caché L3 (probablemente en 1 o 2 formas solo para que el la cantidad total de L3 disponible para nta
prefetches es limitada).
Esto fue probado en el cliente Skylake , pero creo que este comportamiento básico probablemente se extienda probablemente hacia Sandy Bridge y antes (según la redacción en la guía de optimización Intel), y también hacia Kaby Lake y arquitecturas posteriores basadas en el cliente Skylake. Entonces, a menos que esté usando una parte Skylake-SP o Skylake-X, o una CPU extremadamente antigua, este es probablemente el comportamiento que puede esperar de la prefetchnta
.
Servidor Skylake
El único chip Intel reciente que se sabe que tiene un comportamiento diferente es el servidor Skylake (utilizado en Skylake-X, Skylake-SP y algunas otras líneas). Esto tiene una arquitectura L2 y L3 considerablemente modificada, y el L3 ya no incluye el L2 mucho más grande. Para este chip, parece que prefetchnta
omite las cachés L2 y L3, por lo que en esta arquitectura la contaminación del caché está limitada a la L1.
Este comportamiento fue informado por el usuario Mysticial en un comentario . La desventaja, como se señaló en esos comentarios es que esto hace que prefetchnta
sea mucho más frágil: si obtienes la distancia de precaptura o el tiempo incorrecto (especialmente fácil cuando está involucrado el hyperthreading y el núcleo hermano está activo), y los datos son desalojados de L1 antes Si usa, va a volver a la memoria principal en lugar de la L3 en las arquitecturas anteriores.
1 Recientes aquí probablemente signifiquen algo en la última década, pero no quiero decir que el hardware anterior no admitiera la captación previa no temporal: es posible que el soporte vuelva inmediatamente a la introducción del prefetchnta
pero no lo hago tener el hardware para verificar eso y no puede encontrar una fuente confiable de información existente en él.
2 Normal aquí solo significa memoria WB (writeback), que es la que trata la memoria en el nivel de aplicación la abrumadora mayoría de las veces.
3 Específicamente, las instrucciones de la tienda NT son movnti
para registros de propósito general y las movntd*
y movntp*
para registros SIMD.
Estoy al tanto de múltiples preguntas sobre este tema, sin embargo, no he visto respuestas claras ni mediciones de referencia. Creé un programa simple que funciona con dos matrices de números enteros. La primera matriz a
es muy grande (64 MB) y la segunda matriz b
es pequeña para caber en la memoria caché L1. El programa itera sobre a
y agrega sus elementos a los elementos correspondientes de b
en un sentido modular (cuando se alcanza el final de b
, el programa comienza de nuevo). El número medido de errores de caché L1 para diferentes tamaños de b
es el siguiente:
Las mediciones se realizaron en una CPU de tipo Haswell Xeon E5 2680v3 con caché de datos L1 de 32 kb. Por lo tanto, en todos los casos, b
ajusta en la caché L1. Sin embargo, el número de fallas aumentó considerablemente en alrededor de 16 kB de memoria b
huella. Esto podría esperarse dado que las cargas de b
causan la invalidación de las líneas de caché desde el principio de b
en este punto.
No hay absolutamente ninguna razón para mantener los elementos de a
caché en el caché, solo se usan una vez. Por lo tanto, ejecuto una variante del programa con cargas de datos no temporales, pero el número de errores no cambió. También corro una variante con la captación previa no temporal de a
dato, pero con los mismos resultados.
Mi código de referencia es el siguiente (se muestra la variante sin captación previa no temporal):
int main(int argc, char* argv[])
{
uint64_t* a;
const uint64_t a_bytes = 64 * 1024 * 1024;
const uint64_t a_count = a_bytes / sizeof(uint64_t);
posix_memalign((void**)(&a), 64, a_bytes);
uint64_t* b;
const uint64_t b_bytes = atol(argv[1]) * 1024;
const uint64_t b_count = b_bytes / sizeof(uint64_t);
posix_memalign((void**)(&b), 64, b_bytes);
__m256i ones = _mm256_set1_epi64x(1UL);
for (long i = 0; i < a_count; i += 4)
_mm256_stream_si256((__m256i*)(a + i), ones);
// load b into L1 cache
for (long i = 0; i < b_count; i++)
b[i] = 0;
int papi_events[1] = { PAPI_L1_DCM };
long long papi_values[1];
PAPI_start_counters(papi_events, 1);
uint64_t* a_ptr = a;
const uint64_t* a_ptr_end = a + a_count;
uint64_t* b_ptr = b;
const uint64_t* b_ptr_end = b + b_count;
while (a_ptr < a_ptr_end) {
#ifndef NTLOAD
__m256i aa = _mm256_load_si256((__m256i*)a_ptr);
#else
__m256i aa = _mm256_stream_load_si256((__m256i*)a_ptr);
#endif
__m256i bb = _mm256_load_si256((__m256i*)b_ptr);
bb = _mm256_add_epi64(aa, bb);
_mm256_store_si256((__m256i*)b_ptr, bb);
a_ptr += 4;
b_ptr += 4;
if (b_ptr >= b_ptr_end)
b_ptr = b;
}
PAPI_stop_counters(papi_values, 1);
std::cout << "L1 cache misses: " << papi_values[0] << std::endl;
free(a);
free(b);
}
Lo que me pregunto es si los proveedores de CPU soportan o van a admitir cargas / captaciones no temporales o de otra forma cómo etiquetar algunos datos como no-estar-retenidos en caché (por ejemplo, etiquetarlos como LRU). Hay situaciones, por ejemplo, en HPC, donde escenarios similares son comunes en la práctica. Por ejemplo, en solucionadores lineales iterativos dispersos / eigensolvers, los datos de la matriz suelen ser muy grandes (más grandes que las capacidades de caché), pero los vectores a veces son lo suficientemente pequeños como para caber en el caché L3 o incluso L2. Entonces, nos gustaría mantenerlos allí cueste lo que cueste. Desafortunadamente, la carga de datos matriciales puede causar la invalidación de líneas de caché especialmente x-vector, aunque en cada iteración de solucionador, los elementos de matriz se usan solo una vez y no hay razón para mantenerlos en caché después de que hayan sido procesados.
ACTUALIZAR
Acabo de hacer un experimento similar en un Intel Xeon Phi KNC, mientras que se midió el tiempo de ejecución en lugar de L1 (no encontré una manera de medirlos de manera confiable; PAPI y VTune dieron metricas extrañas). Los resultados están aquí:
La curva naranja representa cargas ordinarias y tiene la forma esperada. La curva azul representa cargas con la llamada de desalojo de llamada (EH) establecida en el prefijo de instrucción y la curva de color gris representa un caso en el que cada línea de caché de a
desalojo manual; ambos trucos habilitados por KNC obviamente funcionaron como queríamos para b
más de 16 kib. El código del ciclo medido es el siguiente:
while (a_ptr < a_ptr_end) {
#ifdef NTLOAD
__m512i aa = _mm512_extload_epi64((__m512i*)a_ptr,
_MM_UPCONV_EPI64_NONE, _MM_BROADCAST64_NONE, _MM_HINT_NT);
#else
__m512i aa = _mm512_load_epi64((__m512i*)a_ptr);
#endif
__m512i bb = _mm512_load_epi64((__m512i*)b_ptr);
bb = _mm512_or_epi64(aa, bb);
_mm512_store_epi64((__m512i*)b_ptr, bb);
#ifdef EVICT
_mm_clevict(a_ptr, _MM_HINT_T0);
#endif
a_ptr += 8;
b_ptr += 8;
if (b_ptr >= b_ptr_end)
b_ptr = b;
}
ACTUALIZACIÓN 2
En Xeon Phi, icpc
generado para la icpc
de la variante de carga normal (curva naranja) para a_ptr
:
400e93: 62 d1 78 08 18 4c 24 vprefetch0 [r12+0x80]
Cuando manualmente (mediante la edición hexadecimal del ejecutable) modifiqué esto a:
400e93: 62 d1 78 08 18 44 24 vprefetchnta [r12+0x80]
Obtuve los resultados deseados, incluso mejores que las curvas azules / grises. Sin embargo, no pude forzar al compilador a generar prefetchnig no temporal para mí, incluso usando #pragma prefetch a_ptr:_MM_HINT_NTA
antes del ciclo :(
Respondo mi propia pregunta ya que encontré la siguiente publicación del Foro Intel para desarrolladores, que tiene sentido para mí. Fue escrito por John McCalpin:
Los resultados para los procesadores convencionales no son sorprendentes: en ausencia de una verdadera memoria "scratchpad", no está claro que sea posible diseñar una implementación de un comportamiento "no temporal" que no esté sujeto a sorpresas desagradables. Dos enfoques que se han utilizado en el pasado son (1) cargar la línea de caché, pero marcándola LRU en lugar de MRU, y (2) cargando la línea de caché en un "conjunto" específico de la caché asociativa establecida. En cualquier caso, es relativamente fácil generar situaciones en las que la memoria caché omite los datos antes de que el procesador termine de leerlos.
Ambos enfoques arriesgan la degradación del rendimiento en los casos que operan en más de un número pequeño de matrices, y son mucho más difíciles de implementar sin "errores" cuando se considera HyperThreading.
En otros contextos, he defendido la implementación de instrucciones de "carga múltiple" que garantizarían que todo el contenido de una línea de caché se copiará a registros atómicamente. Mi razonamiento es que el hardware absolutamente garantiza que la línea de caché se mueva atómicamente y que el tiempo requerido para copiar el resto de la línea de caché a los registros fue muy pequeño (un extra de 1-3 ciclos, dependiendo de la generación del procesador) que podría ser implementado con seguridad como una operación atómica.
Comenzando con Haswell, el núcleo puede leer 64 bytes en un solo ciclo (2 lecturas AVX alineadas de 256 bits), por lo que la exposición a efectos secundarios no deseados se reduce aún más.
Comenzando con KNL, las cargas de línea de caché completa (alineadas) deben ser atómicas "naturalmente", ya que las transferencias desde la caché de datos L1 al núcleo son líneas de caché completas y todos los datos se colocan en el registro AVX-512 de destino. (¡Esto no significa que Intel garantice la atomicidad en la implementación! No tenemos visibilidad de los casos de esquinas horribles que los diseñadores tienen que explicar, pero es razonable concluir que la mayoría de las cargas de 512 bits alineadas en el tiempo ocurrirán atómicamente.) Con esta atomicidad "natural" de 64 bytes, algunos de los trucos utilizados en el pasado para reducir la contaminación de la memoria caché debido a cargas "no temporales" pueden merecer otra mirada ....
La instrucción MOVNTDQA está destinada principalmente a la lectura de rangos de direcciones que están mapeados como "Combinación de escritura" (WC), y no para la lectura de la memoria normal del sistema que está mapeada como "Write-Back" (WB). La descripción en el Volumen 2 del SWDM dice que una implementación "puede" hacer algo especial con MOVNTDQA para regiones WB, pero el énfasis está en el comportamiento para el tipo de memoria WC.
El tipo de memoria "Write-Combining" casi nunca se usa para la memoria "real"; se usa casi exclusivamente para regiones IO asignadas por memoria.
Consulte aquí para ver la publicación completa: software.intel.com/en-us/forums/intel-isa-extensions/topic/…