c++ concurrency c++11 compiler-optimization memory-model

C++ 0x modelo de memoria y cargas/tiendas especulativas.



concurrency c++11 (2)

Así que estaba leyendo sobre el modelo de memoria que forma parte del próximo estándar C ++ 0x. Sin embargo, estoy un poco confundido acerca de algunas de las restricciones para lo que el compilador tiene permitido hacer, específicamente acerca de las cargas especulativas y las tiendas.

Para empezar, algunas de las cosas relevantes:

Páginas de Hans Boehm sobre hilos y el modelo de memoria en C ++ 0x

Boehm, "Los subprocesos no se pueden implementar como una biblioteca"

Boehm y Adve, "Fundamentos del modelo de memoria de concurrencia de C ++"

Sutter, "Prism: un modelo de memoria secuencial basado en principios para plataformas de código nativo de Microsoft", N2197

Boehm, "Consecuencias del compilador del modelo de memoria concurrente", N2338

Ahora, la idea básica es esencialmente "Consistencia Secuencial para Programas Libres de Carrera de Datos", que parece ser un compromiso decente entre la facilidad de programación y permitir que el compilador y las oportunidades de hardware se optimicen. Se define una carrera de datos si no se ordenan dos accesos a la misma ubicación de memoria por diferentes hilos, al menos uno de ellos almacena en la ubicación de memoria, y al menos uno de ellos no es una acción de sincronización. Implica que todo el acceso de lectura / escritura a los datos compartidos debe realizarse a través de algún mecanismo de sincronización, como mutexes u operaciones en variables atómicas (bueno, es posible operar en las variables atómicas con un orden de memoria relajado solo para expertos , pero el valor predeterminado proporciona para consistencia secuencial).

A la luz de esto, estoy confundido acerca de las restricciones sobre las cargas / tiendas espurias o especulativas en las variables compartidas comunes. Por ejemplo, en N2338 tenemos el ejemplo.

switch (y) { case 0: x = 17; w = 1; break; case 1: x = 17; w = 3; break; case 2: w = 9; break; case 3: x = 17; w = 1; break; case 4: x = 17; w = 3; break; case 5: x = 17; w = 9; break; default: x = 17; w = 42; break; }

en el cual el compilador no puede transformarse en

tmp = x; x = 17; switch (y) { case 0: w = 1; break; case 1: w = 3; break; case 2: x = tmp; w = 9; break; case 3: w = 1; break; case 4: w = 3; break; case 5: w = 9; break; default: w = 42; break; }

ya que si y == 2 hay una escritura falsa en x, lo que podría ser un problema si otro hilo estuviera actualizando simultáneamente x. Pero, ¿por qué es esto un problema? Esta es una carrera de datos, que está prohibida de todos modos; en este caso, el compilador simplemente lo empeora al escribir en x dos veces, pero incluso una sola escritura sería suficiente para una carrera de datos, ¿no? Es decir, un programa de C ++ 0x adecuado necesitaría sincronizar el acceso a x, en cuyo caso ya no habría una carrera de datos, y el almacenamiento no esencial tampoco sería un problema.

También estoy confundido acerca del Ejemplo 3.1.3 en N2197 y algunos de los otros ejemplos, pero tal vez una explicación para el problema anterior también lo explique.

EDITAR: La Respuesta:

La razón por la que los almacenes especulativos son un problema es que en el ejemplo de cambio anterior, el programador podría haber elegido adquirir condicionalmente el bloqueo que protege x solo si y = = Por lo tanto, el almacén especulativo podría introducir una carrera de datos que no existía en El código original, y por lo tanto la transformación está prohibida. El mismo argumento se aplica al Ejemplo 3.1.3 en N2197 también.


No estoy familiarizado con todas las cosas a las que se refiere, pero observe que en el caso y == 2, en el primer bit del código, x no está escrito en absoluto (o leído, para el caso). En el segundo bit de código, se escribe dos veces. Esto es más una diferencia que solo escribir una vez en lugar de escribir dos veces (al menos, está en los modelos de subprocesos existentes como pthreads). Además, almacenar un valor que de otra manera no se almacenaría en absoluto es más una diferencia que solo almacenar una vez en lugar de almacenar dos veces. Por estas dos razones, no desea que los compiladores simplemente reemplacen un no-op con tmp = x; x = 17; x = tmp; tmp = x; x = 17; x = tmp; .

Supongamos que el hilo A quiere asumir que ningún otro hilo modifica x. Es razonable querer que se le permita esperar que si y es 2, y escribe un valor en x, luego lo lee, recuperará el valor que ha escrito. Pero si el subproceso B ejecuta simultáneamente su segundo bit de código, entonces el subproceso A podría escribir en x y luego leerlo, y recuperar el valor original, porque el subproceso B guardó "antes de" escribir y restauró "después de". O podría recuperar 17, porque el subproceso B almacenó 17 "después" de la escritura, y almacenó tmp nuevamente "después de" el subproceso A lee. El subproceso A puede hacer la sincronización que desee, y no ayudará, porque el subproceso B no está sincronizado. La razón por la que no está sincronizado (en el caso de y == 2) es que no está usando x. Por lo tanto, el concepto de si un bit de código en particular "usa x" es importante para el modelo de subprocesamiento, lo que significa que a los compiladores no se les puede permitir cambiar el código para usar x cuando "no debería".

En resumen, si se permitiera la transformación que propones, introduciendo una escritura falsa, nunca sería posible analizar un poco de código y concluir que no modifica x (o cualquier otra ubicación de memoria). Hay una serie de expresiones idiomáticas convenientes que, por lo tanto, serían imposibles, como compartir datos inmutables entre hilos sin sincronización.

Entonces, aunque no estoy familiarizado con la definición de "carrera de datos" de C ++ 0x, asumo que incluye algunas condiciones donde los programadores pueden asumir que un objeto no está escrito y que esta transformación violaría esas condiciones. Especulo que si y == 2, entonces su código original, junto con el código concurrente: x = 42; x = 1; z = x x = 42; x = 1; z = x x = 42; x = 1; z = x en otro hilo, no se define como una carrera de datos. O al menos si es una carrera de datos, no es una que permita que z termine con un valor de 17 o 42.

Tenga en cuenta que en este programa, el valor 2 en y podría usarse para indicar, "hay otros subprocesos en ejecución: no modifique x, porque no estamos sincronizados aquí, por lo que eso introduciría una carrera de datos". Quizás la razón por la que no hay ninguna sincronización en absoluto, es que en todos los demás casos de y, no hay otros subprocesos que se ejecutan con acceso a x. Me parece razonable que C ++ 0x quiera admitir código como este:

if (single_threaded) { x = 17; } else { sendMessageThatSafelySetsXTo(17); }

Claramente entonces, no quieres que se transforme en:

tmp = x; x = 17; if (!single_threaded) { x = tmp; sendMessageThatSafelySetsXTo(17); }

Básicamente, es la misma transformación que en su ejemplo, pero con solo 2 casos, en lugar de ser suficiente para que parezca una buena optimización de tamaño de código.


Si y==2 , y otro hilo modifica o lee x , ¿cómo existe una condición de carrera en la muestra original? Este hilo nunca toca x , por lo que otros hilos pueden hacerlo libremente.

Pero con la versión reordenada, nuestro hilo modifica x , aunque solo sea temporalmente, por lo tanto, si otro hilo también lo manipula, ahora tenemos una condición de carrera en la que antes no había ninguno.