¿La explicación del orden relajado es errónea en la preferencia?

13

En la documentación de std::memory_ordercppreference.com hay un ejemplo de pedido relajado:

Pedidos relajados

Las operaciones atómicas etiquetadas memory_order_relaxedno son operaciones de sincronización; no imponen un orden entre los accesos concurrentes de memoria. Solo garantizan la atomicidad y la coherencia del orden de modificación.

Por ejemplo, con x e y inicialmente cero,

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

se permite producir r1 == r2 == 42 porque, aunque A se secuencia antes de B dentro del hilo 1 y C se secuencia antes de D dentro del hilo 2, nada impide que D aparezca antes de A en el orden de modificación de y, y B de apareciendo antes de C en el orden de modificación de x. El efecto secundario de D en y podría ser visible para la carga A en el subproceso 1, mientras que el efecto secundario de B en x podría ser visible para la carga C en el subproceso 2. En particular, esto puede ocurrir si D se completa antes de C en subproceso 2, ya sea debido a la reordenación del compilador o en tiempo de ejecución.

dice "C está secuenciado antes que D dentro del hilo 2".

De acuerdo con la definición de secuenciado antes, que se puede encontrar en el Orden de evaluación , si A se secuencia antes que B, la evaluación de A se completará antes de que comience la evaluación de B. Dado que C se secuencia antes de D dentro del hilo 2, C debe completarse antes de que D comience, por lo tanto, la parte de condición de la última oración de la instantánea nunca se cumplirá.

abigaile
fuente
¿Su pregunta es específicamente sobre C ++ 11?
curioso
no, también se aplica a c ++ 14,17. Sé que tanto el compilador como la CPU pueden reordenar C con D. Pero si ocurre el reordenamiento, C no se puede completar antes de que comience D. Así que creo que hay un mal uso de la terminología en la oración "A está secuenciada antes de B dentro del hilo 1 y C está secuenciada antes de D dentro del hilo 2". Es más preciso decir "En el código, A se COLOCA ANTES de B dentro del hilo 1 y C se COLOCA ANTES de D dentro del hilo 2". El objetivo de esta pregunta es confirmar este pensamiento
abigaile el
Nada se define en términos de "reordenamiento".
curioso

Respuestas:

13

Creo que la preferencia es correcta. Creo que esto se reduce a la regla "como si" [intro.execution] / 1 . Los compiladores solo están obligados a reproducir el comportamiento observable del programa descrito por su código. Una relación secuenciada antes solo se establece entre evaluaciones desde la perspectiva del hilo en el que se realizan estas evaluaciones [intro.execution] / 15 . Eso significa que cuando dos evaluaciones secuenciadas una después de la otra aparecen en algún subproceso, el código que realmente se ejecuta en ese subproceso debe comportarse como si lo que fuera que hiciera la primera evaluación realmente afectara lo que haga la segunda evaluación. Por ejemplo

int x = 0;
x = 42;
std::cout << x;

debe imprimir 42. Sin embargo, el compilador en realidad no tiene que almacenar el valor 42 en un objeto xantes de leer el valor de ese objeto para imprimirlo. También puede recordar que el último valor que se almacenó xfue 42 y luego simplemente imprimir el valor 42 directamente antes de hacer un almacenamiento real del valor 42 x. De hecho, si xes una variable local, también puede rastrear qué valor se asignó a esa variable por última vez en cualquier punto y nunca crear un objeto o almacenar el valor 42. No hay forma de que el hilo marque la diferencia. El comportamiento siempre será como si hubiera una variable y como si el valor 42 estuviera realmente almacenado en un objeto x antessiendo cargado desde ese objeto. Pero eso no significa que el código de máquina generado tenga que almacenar y cargar cualquier cosa en cualquier lugar. Todo lo que se requiere es que el comportamiento observable del código de máquina generado sea indistinguible de cuál sería el comportamiento si todas estas cosas realmente sucedieran.

Si nos fijamos en

r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

entonces sí, C se secuencia antes que D. Pero cuando se ve desde este hilo de forma aislada, nada de lo que C afecta el resultado de D. Y nada de lo que hace D cambiaría el resultado de C. La única forma en que uno podría afectar al otro sería como consecuencia indirecta de que algo suceda en otro hilo. Sin embargo, al especificar std::memory_order_relaxed, usted declaró explícitamenteque el orden en que la carga y el almacenamiento son observados por otro hilo es irrelevante. Como ningún otro subproceso puede observar la carga y el almacenamiento en un orden particular, no hay nada que otro subproceso pueda hacer para que C y D se afecten entre sí de manera consistente. Por lo tanto, el orden en que se realizan realmente la carga y el almacenamiento es irrelevante. Por lo tanto, el compilador es libre de reordenarlos. Y, como se menciona en la explicación debajo de ese ejemplo, si el almacenamiento de D se realiza antes de la carga de C, entonces r1 == r2 == 42 puede ocurrir ...

Michael Kenzel
fuente
Entonces, esencialmente, el estándar establece que C debe suceder antes que D , pero el compilador cree que no se puede probar si C o D sucedió después y, debido a la regla as-if, los reordena de todos modos, ¿correcto?
Fureeish
44
@Fureeish No. C debe suceder antes de D en lo que respecta al hilo en el que suceden Observar desde otro contexto puede ser inconsistente con esa visión.
Deduplicador
55
@curiousguy Esta afirmación parece similar a sus otros evangelismos anteriores de C ++ .
Carreras de ligereza en órbita el
1
@curiousguy Michael ha publicado una larga explicación junto con enlaces a los capítulos relevantes de la norma.
Carreras de ligereza en órbita el
2
@curiousguy La norma hace una etiqueta de la misma de disposiciones "la regla como si" en una nota al pie: "Esta disposición es a veces llamado el‘como si’regla" intro.execution
Caleth
1

A veces es posible ordenar una acción en relación con otras dos secuencias de acciones, sin implicar ningún orden relativo de las acciones en esas secuencias entre sí.

Supongamos, por ejemplo, que uno tiene los siguientes tres eventos:

  • almacenar 1 a p1
  • cargar p2 en temp
  • almacenar 2 a p3

y la lectura de p2 se ordena independientemente después de la escritura de p1 y antes de la escritura de p3, pero no existe un orden particular en el que participen tanto p1 como p3. Dependiendo de lo que se haga con p2, puede ser poco práctico para un compilador diferir p1 más allá de p3 y aún así lograr la semántica requerida con p2. Supongamos, sin embargo, que el compilador sabía que el código anterior era parte de una secuencia más grande:

  • almacenar 1 a p2 [secuenciado antes de la carga de p2]
  • [hacer lo anterior]
  • almacenar 3 en p1 [secuenciado después de la otra tienda a p1]

En ese caso, podría determinar que podría reordenar la tienda a p1 después del código anterior y consolidarla con la siguiente tienda, lo que da como resultado un código que escribe p3 sin escribir p1 primero:

  • establecer temperatura a 1
  • almacenar temperatura a p2
  • almacenar 2 a p3
  • almacenar 3 a p1

Aunque parezca que las dependencias de datos causarían que ciertas partes de las relaciones de secuenciación se comporten de forma transitiva, un compilador puede identificar situaciones en las que no existen dependencias de datos aparentes y, por lo tanto, no tendría los efectos transitivos que uno esperaría.

Super gato
fuente
1

Si hay dos declaraciones, el compilador generará código en orden secuencial, por lo que el código de la primera se colocará antes de la segunda. Pero los cpus tienen canales internos y pueden ejecutar operaciones de ensamblaje en paralelo. La declaración C es una instrucción de carga. Mientras se recupera la memoria, la canalización procesará las siguientes instrucciones y, dado que no dependen de la instrucción de carga, podrían terminar ejecutándose antes de que C finalice (por ejemplo, los datos de D estaban en caché, C en la memoria principal).

Si el usuario realmente necesita que las dos instrucciones se ejecuten secuencialmente, se pueden usar operaciones de ordenamiento de memoria más estrictas. En general, a los usuarios no les importa mientras el programa sea lógicamente correcto.

edwinc
fuente
-10

Lo que creas es igualmente válido. El estándar no dice qué se ejecuta secuencialmente, qué no y cómo se puede mezclar .

Depende de usted, y de cada programador, crear una semántica consistente además de ese desastre, un trabajo digno de múltiples doctorados.

curioso
fuente