protocolo multiprocesadores modelos falta computadoras coherencia cache arquitectura c++ multithreading x86 multicore cpu-cache

c++ - multiprocesadores - modelos de coherencia de cache



¿Puedo forzar la coherencia de la memoria caché en una CPU x86 multinúcleo? (9)

La otra semana, escribí una pequeña clase de subprocesos y un canal de mensajes unidireccional para permitir la comunicación entre subprocesos (dos canalizaciones por hilo, obviamente, para comunicación bidireccional). Todo funcionó bien en mi Athlon 64 X2, pero me preguntaba si me encontraría con algún problema si ambos subprocesos estuvieran buscando la misma variable y el valor de caché local para esta variable en cada núcleo no estuviera sincronizado.

Sé que la palabra clave volátil obligará a una variable a actualizar desde la memoria, pero ¿hay alguna forma de que los procesadores x86 multinúcleo fuercen la sincronización de las memorias caché de todos los núcleos? ¿Es esto algo de lo que tengo que preocuparme, o el uso volátil y adecuado de mecanismos de bloqueo livianos (estaba usando _InterlockedExchange para establecer mis variables volátiles de tubería) maneja todos los casos en que deseo escribir código "bloqueado" para CPU x86 multinúcleo?

Ya conozco y he usado Secciones críticas, Mutexes, Eventos, etc. Me pregunto si existen intrínsecos x86 de los que no estoy al tanto de qué fuerza o si puedo usarlos para forzar la coherencia de la memoria caché.


La coherencia de la caché está garantizada entre los núcleos debido al protocolo MESI empleado por los procesadores x86. Solo debe preocuparse por la coherencia de la memoria cuando se trata de hardware externo que puede acceder a la memoria mientras los datos aún se ubican en los cachés de los núcleos. No parece que sea su caso aquí, sin embargo, ya que el texto sugiere que está programando en un país de usuarios.


No especificó qué compilador está utilizando, pero si está en Windows, eche un vistazo a este artículo aquí . También eche un vistazo a las funciones de sincronización disponibles aquí . Es posible que desee tener en cuenta que, en general, la volatile no es suficiente para hacer lo que quiere hacer, pero en VC 2005 y 2008, se le agregan semánticas no estándar que agregan barreras implícitas a la memoria en lectura y escritura.

Si quieres que las cosas sean portátiles, vas a tener un camino mucho más difícil por delante.


Volátil no lo hará. En C ++, volátil solo afecta a las optimizaciones del compilador, como almacenar una variable en un registro en lugar de memoria, o eliminarla por completo.


Hay una serie de artículos que explican las arquitecturas de memoria modernas aquí , incluidos los cachés Intel Core2 y muchos más temas de arquitectura moderna.

Los artículos son muy legibles y están bien ilustrados. ¡Disfruta!


Herb Sutter parecía simplemente sugerir que cualesquiera dos variables deberían residir en líneas de caché separadas. Lo hace en su cola simultánea con relleno entre sus bloqueos y punteros de nodo.

Editar: si está utilizando el compilador Intel o GCC, puede usar los comandos integrados atómicos , que parecen hacer todo lo posible para adelantarse a la memoria caché cuando sea posible.


Hay varias subpreguntas en su pregunta, así que las responderé a mi leal saber y entender.

  1. Actualmente no existe una forma portátil de implementar interacciones sin bloqueo en C ++. La propuesta de C ++ 0x resuelve esto al introducir la biblioteca atómica.
  2. No se garantiza que Volátil brinde atomicidad en un multinúcleo y su implementación es específica del proveedor.
  3. En el x86, no necesita hacer nada especial, excepto declarar las variables compartidas como volátiles para evitar algunas optimizaciones del compilador que pueden romper el código multiproceso. Volatile le dice al compilador que no almacene valores en caché.
  4. Hay algunos algoritmos (Dekker, por ejemplo) que no funcionarán incluso en un x86 con variables volátiles.
  5. A menos que sepa con certeza que pasar el acceso a los datos entre hilos es un cuello de botella de rendimiento importante en su programa, evite las soluciones sin bloqueo. Usa datos de paso por valor o bloqueos.


No necesita preocuparse por la coherencia del caché. El hardware se encargará de eso. Lo que puede necesitar preocuparse es problemas de rendimiento debido a esa coherencia de caché.

Si el núcleo # 1 escribe en una variable y el núcleo # 2 lee esa misma variable, el procesador se asegurará de que la memoria caché para el núcleo # 2 se actualice. Como toda la línea de caché (64 bytes) debe leerse desde la memoria, tendrá algún costo de rendimiento. En este caso, es inevitable. Este es el comportamiento deseado.

El problema es que cuando tiene múltiples variables en la misma línea de caché, el procesador puede perder más tiempo manteniendo las cachés sincronizadas, incluso si los núcleos están leyendo / escribiendo diferentes variables dentro de la misma línea de caché. Ese costo se puede evitar asegurándose de que esas variables no estén en la misma línea de caché. Este efecto se conoce como Uso compartido falso ya que obliga a los procesadores a sincronizar los valores de los objetos que realmente no se comparten entre los subprocesos.


volatile solo obliga a su código a volver a leer el valor, no puede controlar de dónde se lee el valor. Si el valor fue leído recientemente por su código, entonces probablemente estará en la memoria caché, en cuyo caso volátil obligará a volver a leer desde la memoria caché, NO desde la memoria.

No hay muchas instrucciones de coherencia de caché en x86. Hay instrucciones de captación previa como prefetchnta , pero eso no afecta a la semántica de orden de memoria. Solía ​​implementarse al traer el valor a la memoria caché L1 sin contaminar L2, pero las cosas son más complicadas para los diseños modernos de Intel con una gran caché L3 compartida e inclusiva .

Las CPU x86 usan una variación en el protocolo MESI (MESIF para Intel, MOESI para AMD) para mantener sus cachés coherentes entre sí (incluidas las cachés privadas L1 de diferentes núcleos). Un núcleo que desea escribir una línea de caché debe forzar a otros núcleos a invalidar su copia antes de que pueda cambiar su propia copia del estado Compartido al estado Modificado.

No necesita ninguna instrucción de cerca (como MFENCE) para generar datos en un subproceso y consumirlos en otro en x86, porque las cargas / tiendas x86 tienen una semántica de adquisición / liberación incorporada. Usted necesita MFENCE (barrera completa) para obtener consistencia secuencial. (Una versión anterior de esta respuesta sugería que clflush era necesario, lo cual es incorrecto).

Es necesario evitar el reordenamiento en tiempo de compilación , porque el modelo de memoria de C ++ está débilmente ordenado. volatile es una vieja y mala forma de hacer esto; C ++ 11 std :: atomic es una forma mucho mejor de escribir código sin candado.