c++ multithreading c++11 atomic volatile

c++ - ¿Pueden las lecturas volátiles pero no cerradas producir valores obsoletos indefinidamente?(en hardware real)



multithreading c++11 (4)

Aquí está mi opinión, aunque no tengo mucho conocimiento sobre el tema, así que tómalo con un grano de sal.

El efecto de palabra clave volatile podría ser dependiente del compilador, pero supondré que realmente hace lo que intuitivamente esperamos de él, es decir, evitar aliasing u otra optimización que no permita al usuario inspeccionar el valor de la variable en un depurador en cualquier punto de ejecución durante la vida de esa variable. Eso es bastante cercano (y probablemente lo mismo) a esa respuesta sobre el significado de volátil.

La implicación directa es que cualquier bloque de código que acceda a la variable volatile v deberá enviarlo a la memoria tan pronto como lo haya modificado. Las vallas lo harán para que suceda en orden con otras actualizaciones, pero de cualquier manera, habrá una tienda en v en la salida del ensamblaje si v se modifica en el nivel de origen.

De hecho, la pregunta que hace es: si v , cargada en un registro, no ha sido modificada por algún cálculo, lo que obliga a la CPU a ejecutar una lectura de v nuevo a cualquier registro, en lugar de simplemente reutilizar el valor que ya tiene más temprano.

Creo que la respuesta es que la CPU no puede asumir que una celda de memoria no ha cambiado desde su última lectura. El acceso a la memoria, incluso en un sistema de núcleo único, no está estrictamente reservado a la CPU. Muchos otros subsistemas pueden acceder a él de lectura-escritura (ese es el principio detrás de DMA ).

La optimización más segura que una CPU probablemente puede hacer es verificar si el valor cambió en caché o no, y usar eso como una pista del estado de v en la memoria. Las cachés deben mantenerse sincronizadas. con memoria gracias a los mecanismos de invalidación de caché adjuntos con DMA. Con esa condición, el problema vuelve a la coherencia de la memoria caché en multinúcleo , y "escribir después de escribir" para situaciones de subprocesamiento múltiple . Ese último problema no puede manejarse de manera efectiva con variables volatile simples, ya que su operación de modificación no es atómica, como usted ya sabe.

Al responder a esta pregunta surgió una pregunta sobre la situación del OP de la que no estaba seguro: se trata principalmente de una cuestión de arquitectura del procesador, pero también de una pregunta sobre el modelo de memoria C ++ 11.

Básicamente, el código del OP estaba girando infinitamente a niveles de optimización más altos debido al siguiente código (ligeramente modificado para simplificar):

while (true) { uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable if (ov & MASK) { continue; } if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) { break; } }

donde __sync_val_compare_and_swap() es GCC''s atomic CAS built-in. GCC (razonablemente) optimizó esto en un bucle infinito en el caso de que bits_ & mask se detectara como true antes de ingresar al bucle, omitiendo por completo la operación CAS, por lo que sugerí el siguiente cambio (que funciona):

while (true) { uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable if (ov & MASK) { __sync_synchronize(); continue; } if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) { break; } }

Después de responder, OP señaló que cambiar bits_ a volatile uint8_t parece funcionar también. Sugerí no ir por esa ruta, ya que normalmente no se debe usar volatile para la sincronización, y de todos modos no parece haber muchas desventajas al usar una valla aquí.

Sin embargo, lo pensé más, y en este caso la semántica es tal que en realidad no importa si la comprobación de ov & MASK se basa en un valor obsoleto, siempre que no se base en un valor indefinido (es decir, como siempre que el bucle se interrumpa eventualmente), ya que el intento real de actualizar bits_ está sincronizado. Entonces, ¿es lo suficientemente volatile aquí para garantizar que este ciclo finalice eventualmente si bits_ se actualiza por otro hilo de modo que bits_ & MASK == false , para cualquier procesador existente? En otras palabras, en ausencia de una valla de memoria explícita, ¿es prácticamente posible que las lecturas no optimizadas por el compilador sean efectivamente optimizadas por el procesador, indefinidamente? ( EDITAR: Para ser claro, pregunto aquí qué hardware moderno podría hacer dado el supuesto de que las lecturas son emitidas en un ciclo por el compilador, por lo que técnicamente no es una pregunta de lenguaje aunque es conveniente expresarla en términos de semántica de C ++ .)

Ese es el ángulo de hardware, pero para actualizarlo ligeramente y hacer que también sea una pregunta que pueda responderse sobre el modelo de memoria C ++ 11, considere la siguiente variación del código anterior:

// bits_ is "std::atomic<unsigned char>" unsigned char ov = bits_.load(std::memory_order_relaxed); while (true) { if (ov & MASK) { ov = bits_.load(std::memory_order_relaxed); continue; } // compare_exchange_weak also updates ov if the exchange fails if (bits_.compare_exchange_weak(ov, ov | MASK, std::memory_order_acq_rel)) { break; } }

cppreference afirma que std::memory_order_relaxed implica "ninguna restricción en la reordenación de los accesos de memoria en torno a la variable atómica", por lo que independientemente de lo que el hardware real haga o deje de hacer, implica que bits_.load(std::memory_order_relaxed) técnicamente nunca podría leer un valor actualizado después de bits_ se actualiza en otro subproceso en una implementación conforme?

EDIT: encontré esto en el estándar (29.4 p13):

Las implementaciones deben hacer que las tiendas atómicas sean visibles para las cargas atómicas dentro de un período de tiempo razonable.

Así que, aparentemente, esperar ("infinitamente largo") para obtener un valor actualizado es (¿sobre todo?) Imposible, pero no hay una garantía sólida de un intervalo de tiempo específico de frescura que no sea "razonable"; aún así, la pregunta sobre el comportamiento real del hardware se mantiene.


C ++ 11 atomics se ocupa de tres cuestiones:

  1. asegurando que un valor completo sea leído o escrito sin un interruptor de hilo; esto previene el desgarro

  2. asegurando que el compilador no vuelva a ordenar las instrucciones dentro de un hilo a través de una lectura o escritura atómica; esto asegura el orden dentro del hilo.

  3. asegurando (para las elecciones apropiadas de parámetros de orden de memoria) que los datos escritos dentro de un hilo antes de una escritura atómica serán vistos por un hilo que lea la variable atómica y vea el valor que se escribió. Esto es visibilidad.

Cuando utiliza memory_order_relaxed , no obtiene una garantía de visibilidad desde la tienda o carga relajada. Obtienes las dos primeras garantías.

Las implementaciones "deberían" (es decir, se recomienda) que las escrituras de memoria sean visibles dentro de un período de tiempo razonable, incluso con un orden relajado. Eso es lo mejor que se puede decir; tarde o temprano estas cosas deberían aparecer.

Entonces, sí, formalmente, una implementación que nunca hizo que las escrituras relajadas sean visibles para las lecturas relajadas se ajusta a la definición del lenguaje. En la práctica, esto no sucederá.

En cuanto a lo volatile , pregunte a su proveedor del compilador. Depende de la implementación.


Es técnicamente legal que las cargas std::memory_order_relaxed nunca devuelvan nunca un nuevo valor para la carga. En cuanto a si alguna implementación hará esto, no tengo ni idea.

Referencia: http://www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/ "El único requisito es que los accesos a una única variable atómica del mismo hilo no pueden ser reordenado: una vez que un hilo dado ha visto un valor particular de una variable atómica, una lectura posterior por ese hilo no puede recuperar un valor anterior de la variable ".


Si los procesadores no tienen un protocolo de coherencia de caché o lo tienen de manera muy simple, entonces puede ''optimizar'' las cargas que obtienen datos obsoletos del caché. Ahora la mayoría de las CPU multinúcleo modernas implementan el protocolo de coherencia de caché. Sin embargo, el ARM antes de A9 no lo tenía. Las arquitecturas sin CPU también pueden no tener coherencia de caché (aunque podrían argumentar que no se adhieren al modelo de memoria C ++).

Otro problema es que muchas arquitecturas (incluyendo ARM y x86) permiten reordenar el acceso a la memoria. No sé si los procesadores son lo suficientemente inteligentes como para notar los accesos reiterados a la misma dirección, pero lo dudo (cuesta poco tiempo y espacio para casos excepcionales, como el compilador debería ser capaz de notarlo, con pequeños beneficios, ya que es probable que los accesos posteriores ser L1 hits) pero técnicamente puede especular que se tomará una rama y puede reordenar el segundo acceso antes del primero (improbable, pero si leo correctamente el manual de Intel y ARM, esto está permitido).

Finalmente, hay dispositivos externos que no se adhieren a la coherencia-coherencia. Si la CPU se comunica mediante IO / DMA mapeado en memoria, la página debe marcarse como no cachable (de lo contrario, en L1 / L2 / L3 / ... la caché se ordenaría en datos). En tales casos, el procesador no volverá a ordenar las lecturas y escrituras (para más detalles, consulte el manual del procesador, puede tener un control más preciso), el compilador puede, por lo que debe usar volatile . Sin embargo, como los átomos atómicos suelen estar basados ​​en caché, no los necesita ni puede usarlos.

Me temo que no puedo responder si una coherencia de caché tan fuerte estará disponible en futuros procesadores. Sugiero seguir estrictamente la especificación ("¿Qué hay de malo en almacenar el puntero en int? Seguramente nadie usará más que 4GiB entonces la dirección 32b es lo suficientemente grande"). La corrección fue respondida por otros, así que no la incluiré.