2011-07-13 8 views
6

En referencia a (un poco anticuado) paper de Hans Boehm, en "Operaciones atómicas". Menciona que el modelo de memoria (propuesto en ese momento) no evitaría que un compilador de optimización combinara una secuencia de cargas, o almacenes, en la misma variable para que se combinara en una sola carga. Su ejemplo es el siguiente (actualizado a la sintaxis actual es de esperar correcta):Combinación de tiendas/cargas de variables atómicas consecutivas

Dado

atomic<int> v; 

El código

while(v.load(memory_order_acquire)) { ... } 

podría optimizarse a:

int a = v.load(memory_order_acquire); 
while(a) { ... } 

Obviamente, esto sería ser malo, como él dice. Ahora mi pregunta es, ya que el documento es un poco viejo, ¿el modelo actual de memoria C++ 0x previene este tipo de optimización, o todavía está técnicamente permitido?

Mi lectura de la norma parece inclinarse hacia su anulación, pero la semántica de uso "adquirir" la hace menos clara. Por ejemplo, si fuera "seq_cst", el modelo parece garantizar que la carga debe participar en un ordenamiento total del acceso y cargar el valor solo una vez parecería violar el orden (ya que rompe la secuencia antes de la relación).

Para adquirir interpreto 29.3.2 para indicar que esta optimización no puede ocurrir, ya que cualquier operación de "liberación" debe ser observada por la operación "adquirir". Hacer solo una adquisición parecería no ser válido.

Entonces mi pregunta es si el modelo actual (en el estándar pendiente) no permitiría este tipo de optimización? Y si es así, ¿qué parte específicamente lo prohíbe? Si no, ¿usar el volatile atómico resuelve el problema?

Y, como beneficio adicional, si la operación de carga tiene un orden "relajado" ¿se permite entonces la optimización?

Respuesta

2

El estándar C++ 0x intenta proscribir esta optimización.

Las palabras relevantes son de 29.3p13:

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

Si el hilo que está cargando solo emite una instrucción de carga, esto se infringe, como si se olvidó de escribir la primera vez, nunca lo verá. No importa qué orden de memoria se utiliza para la carga, es lo mismo para memory_order_seq_cst y memory_order_relaxed.

Sin embargo, la optimización siguiente es animales, a menos que haya algo en el bucle que obliga a un ordenamiento:

while(v.load(memory_order_acquire)) { 
    for(unsigned __temp=0;__temp<100;++__temp) { 
     // original loop body goes here 
    } 
} 

es decir, el compilador puede generar código que ejecuta las cargas reales de forma arbitraria con poca frecuencia, a condición de que todavía los ejecuta. Esto está permitido incluso para memory_order_seq_cst a menos que haya otras operaciones memory_order_seq_cst en el bucle, ya que esto es equivalente a ejecutar 100 iteraciones entre cualquier acceso de memoria por otros hilos.

Como acotación al margen, el uso de memory_order_acquire no tiene el efecto que describes --- no se requiere para ver lanzar operaciones (excepto por 29.3p13 citado anteriormente), al igual que si se hace ver la operación de liberación luego impone restricciones de visibilidad en otros accesos.

+0

Sobre la semántica de la adquisición: Sí, he mal redactado ese bit; la publicación que se muestra establece la relación secuenciada, pero técnicamente no hay garantía de que la haya visto (excepto que tal compilador/CPU podría perder rápidamente todo el mercado). compartir). –

+0

Acerca de 29.3p13, también leí este fragmento y estuve tentado de pensar que es una garantía. Pero debido a que usa el término * tiempo razonable *, no tiene ningún valor y podría simplemente eliminarse del estándar. Es un término sin sentido que significa que no se puede confiar en situaciones de baja latencia. Por ejemplo, en mi proyecto actual, si este tiempo es más de 10s de nanosegundos, no hay ningún valor en absoluto en la operación atómica (digamos en comparación con un bloqueo). –

+0

¿Está de acuerdo, como creo que lo haría, que convertir este atómico en un elemento "volátil" impide que el compilador realice la optimización que ha mostrado? –

0

Desde el mismo papel que establece el vínculo:

Los volátiles garantiza que el número correcto de las operaciones de memoria son realizar.

La norma dice esencialmente la misma:

acceso a los objetos volátiles se evalúan estrictamente de acuerdo con las reglas de la máquina abstracta.

Esto siempre ha sido así, desde el primer compilador de C de Dennis Ritchie, creo. Tiene que ser así porque los registros de E/S mapeados en memoria no funcionarán de otro modo. Para leer dos caracteres de su teclado, necesita leer el registro mapeado de memoria correspondiente dos veces. Si el compilador tuvo una idea diferente sobre el número de lecturas que tiene que realizar, ¡sería una lástima!

+1

Correcto, pero la pregunta es * debe * ponemos 'volátil' en nuestros datos' atomic' para garantizar que se lea correctamente? Los requisitos de sincronización entre hilos parecen implicar que podríamos no hacerlo. Sin embargo, por la misma razón, los atómicos * do * tienen formas volátiles y calificadas de sus funciones. –

+0

Creo que si quieres el código 'while (v.load (memory_order_acquire)) {}' para trabajar de manera más portátil con una garantía absoluta, entonces tienes que usar 'volátil'. No necesita usar 'volátil' en otros casos, como implementar el patrón de doble bloqueo, porque hay un orden total en la secuencia de verificación de bloqueo y el compilador no puede reordenarlo para verificar el bloqueo o lo que sea, incluso sin volátil. O eso es mi comprensión de todos modos. –

Cuestiones relacionadas