[[carry_dependency]] qué significa y cómo implementar

Sep 29 2020

Estaba leyendo sobre [[carry_dependency]] en esta publicación de SO .

Pero lo que no pude entender son las siguientes oraciones en la respuesta aceptada:

"En particular, si un valor leído con memory_order_consume se pasa a una función, entonces sin [[carry_dependency]], entonces el compilador puede tener que emitir una instrucción de límite de memoria para garantizar que se mantenga la semántica de orden de memoria adecuada. Si el parámetro está anotado con [[carry_dependency]], entonces el compilador puede asumir que el cuerpo de la función llevará correctamente la dependencia, y esta cerca puede que ya no sea necesaria.

De manera similar, si una función devuelve un valor cargado con memory_order_consume, o derivado de tal valor, entonces sin [[carry_dependency]] el compilador puede ser requerido para insertar una instrucción de valla para garantizar que se mantenga la semántica apropiada de ordenación de memoria. Con la anotación [[carry_dependency]], es posible que esta valla ya no sea necesaria, ya que la persona que llama ahora es responsable de mantener el árbol de dependencias ".

Vayamos paso a paso:

"si un valor leído con memory_order_consume se pasa a una función, entonces sin [[carry_dependency]], entonces el compilador puede tener que emitir una instrucción de límite de memoria para garantizar que se mantenga la semántica de ordenación de memoria adecuada".

Entonces, para una variable atómica en el modelo de liberación-consumo de memoria cuando la variable atómica se pasa como un parámetro a la función, el compilador introducirá una instrucción de hardware de cerca para que siempre tenga el valor más reciente y actualizado de la variable atómica proporcionada a la función.

Próximo -

"Si el parámetro está anotado con [[carry_dependency]], entonces el compilador puede asumir que el cuerpo de la función llevará correctamente la dependencia, y es posible que esta valla ya no sea necesaria".

Esto me confunde: el valor de la variable atómica ya se consume y, a continuación, ¿qué dependencia tiene la función?

Similar -

"si una función devuelve un valor cargado con memory_order_consume, o derivado de tal valor, entonces sin [[carry_dependency]] el compilador puede ser requerido para insertar una instrucción de cerca para garantizar que se mantenga la semántica apropiada de ordenamiento de memoria. Con [[ carry_dependency]], esta valla puede que ya no sea necesaria, ya que la persona que llama ahora es responsable de mantener el árbol de dependencias ".

A partir del ejemplo, ¿no está claro cuál es el punto que está tratando de exponer acerca de llevar la dependencia?

Respuestas

5 PeterCordes Sep 29 2020 at 06:38

Solo para su información, memory_order_consume(y [[carries_dependency]]) está esencialmente en desuso porque es demasiado difícil para los compiladores implementar de manera eficiente y correcta las reglas de la forma en que C ++ 11 las diseñó. (Y / o porque [[carries_dependency]]y / o kill_dependencyterminaría siendo necesario en todas partes.) Ver P0371R1: Desalentar temporalmente memory_order_consume .

Los compiladores actuales simplemente tratan mo_consumecomo mo_acquire(y, por lo tanto, en las ISA que lo necesitan, colocan una barrera justo después de la carga de consumo). Si desea el rendimiento del orden de dependencia de datos sin barreras, debe engañar al compilador usando mo_relaxedy codificando con cuidado para evitar cosas que harían probable que el compilador creara asm sin una dependencia real. (por ejemplo, RCU de Linux). Consulte C ++ 11: la diferencia entre memory_order_relaxed y memory_order_consume para obtener más detalles y enlaces sobre eso, y la función asm que mo_consumefue diseñada para exponer.

Además, la orden de memoria consume uso en C11 .
Comprender el concepto de ordenación de dependencias (en asm) es básicamente esencial para comprender cómo está diseñada esta característica de C ++.

Cuando se pasa [una] variable atómica como parámetro a la función, el compilador introducirá una instrucción de hardware de valla ...

En primer lugar, no "pasa una variable atómica" a una función; ¿Qué significa eso? Si estuviera pasando un puntero o una referencia a un objeto atómico, la función estaría haciendo su propia carga desde él, y el código fuente de esa función lo usaría memory_order_consumeo no.

Lo relevante es pasar un valor cargado desde una variable atómica con mo_consume. Me gusta esto:

    int tmp = shared_var.load(std::memory_order_consume);
    func(tmp);

funcpuede usar ese argumento como índice en una matriz de atomic<int>para hacer una mo_relaxedcarga. Para que esa carga se ordene por dependencia después de shared_var.loadincluso sin una barrera de memoria, el código gen para funcdebe asegurarse de que la carga tenga una dependencia de datos ASM en el argumento, incluso si el código C ++ hace algo como tmp -= tmp;que los compiladores normalmente solo tratarían el igual que tmp = 0;(matando el valor anterior).

Pero [[carries_dependency]]haría que el compilador aún haga referencia a ese valor cero con una dependencia de datos al implementar algo como array[idx+tmp].

el valor de la variable atómica ya se consume y, entonces, ¿qué dependencia tiene la función?

"Ya consumido" no es un concepto válido. El objetivo de en consumelugar de acquirees que las cargas posteriores se ordenan correctamente porque tienen una dependencia de datos en el mo_consumeresultado de la carga, lo que le permite evitar barreras. Cada carga posterior necesita tal dependencia si desea que se ordene después de la carga original; no tiene sentido decir que un valor "ya se consume".

Si termina insertando una barrera para promover el consumo para adquirir debido a la falta de carry_dependency en una función, las funciones posteriores no necesitarían otra barrera porque podría decir que el valor "ya se adquirió". (Aunque esa no es la terminología estándar. En su lugar, diría código después de que se ordenó la primera barrera después de la carga).


Podría ser útil comprender cómo maneja esto el kernel de Linux, con sus atomics hechos a mano y el conjunto limitado de compiladores que admiten. Busque "dependencia" enhttps://github.com/torvalds/linux/blob/master/Documentation/memory-barriers.txt, y observe la diferencia entre una "dependencia de control" como if(flag) data.load()frente a una dependencia de datos como data[idx].load.

IIRC, incluso C ++ no garantiza el mo_consumeorden de dependencia cuando la dependencia es un condicional como if(x.load(consume)) tmp=y.load();.

Tenga en cuenta que los compiladores a veces convertirán una dependencia de datos en una dependencia de control si solo hay 2 valores posibles, por ejemplo. Esto se rompería mo_consumey sería una optimización que no estaría permitida si el valor provenía de una mo_consumecarga o una [[carries_dependency]]función arg. Esto es parte de por qué es difícil de implementar; requeriría enseñar muchas pasadas de optimización sobre el ordenamiento de la dependencia de datos en lugar de esperar que los usuarios escriban código que no hace cosas que normalmente se optimizarán. (Me gusta tmp -= tmp;)