¿Forzar orden de ejecución de sentencias C?
multithreading synchronization (3)
Entonces, como lo entiendo, el pNext = plog2sizeChunk->pNext
publica el bloque para que otros subprocesos lo puedan ver y debes asegurarte de que vean el indicador de ocupado correcto.
Eso significa que necesita una barrera de memoria unidireccional antes de publicarla (también una antes de leerla en otro hilo, aunque si su código se ejecuta en x86, los obtendrá de forma gratuita) para asegurarse de que los hilos realmente vean el cambio. También necesita uno antes de la escritura para evitar reordenar las escrituras posteriores. No basta con insertar un ensamblaje o usar un volátil compatible (el MSVC volátil ofrece garantías adicionales, pero eso hace una diferencia aquí) no es suficiente. Sí, esto impide que el compilador cambie las lecturas y escrituras, pero la CPU no está vinculada por ello. El mismo reordenar internamente.
Tanto MSVC como gcc tienen intrínsecos / macros para crear barreras de memoria ( ver, por ejemplo, aquí ). MSVC también ofrece garantías más sólidas a los volátiles que son lo suficientemente buenos para su problema. Finalmente, C ++ 11 atómica funcionaría igual de bien, pero no estoy seguro si C en sí tiene alguna forma portátil de garantizar las barreras de memoria.
Tengo un problema con el compilador de MS C que reordena ciertas declaraciones, críticas en un contexto de subprocesamiento múltiple, en altos niveles de optimización. Quiero saber cómo forzar el pedido en lugares específicos y al mismo tiempo utilizar altos niveles de optimización. (En niveles bajos de optimización, este compilador no reordena las declaraciones)
El siguiente código:
ChunkT* plog2sizeChunk=...
SET_BUSY(plog2sizeChunk->pPoolAndBusyFlag); // set "busy" bit on this chunk of storage
x = plog2sizeChunk->pNext;
produce esto:
0040130F 8B 5A 08 mov ebx,dword ptr [edx+8]
00401312 83 22 FE and dword ptr [edx],0FFFFFFFEh
en el que el compilador reordena la escritura en pPoolAndBusyFlag para que se produzca después de la búsqueda de pNext.
SET_BUSY es esencialmente
plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
Creo que el compilador ha decidido con razón que estaba bien reordenar estos accesos porque son para dos miembros separados de la misma estructura, y tal reordenación no afecta los resultados de la ejecución de un solo hilo:
typedef struct chunk_tag{
unsigned pPoolAndBusyFlag; // Contains pointer to owning pool and a busy flag
natural log2size; // holds log2size of the chunk if Busy==false
struct chunk_tag* pNext; // holds pointer to next block of same size
struct chunk_tag* pPrev; // holds pointer to previous block of same size
} ChunkT, *pChunkT;
Para mis propósitos, el pPoolAndBusyFlag tiene que configurarse antes de que otros accesos a esta estructura sean válidos en un contexto multiproceso / multinúcleo. No creo que este acceso en particular sea problemático para mí, pero el hecho de que el compilador pueda reordenar significa que otras partes de mi código pueden tener el mismo tipo de reordenación pero puede ser crítico en esos lugares. (Imagine que las dos declaraciones son actualizaciones para los dos miembros en lugar de una escritura / una lectura). Quiero poder forzar el orden de las acciones.
Lo ideal sería escribir algo como:
plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
#pragma no-reordering // no such directive appears to exist
pNext = plog2sizeChunk->pNext;
He verificado experimentalmente que puedo obtener este efecto de esta manera fea:
plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
asm { xor eax, eax } // compiler won''t optimize past asm block
pNext = plog2sizeChunk->pNext;
da
0040130F 83 22 FE and dword ptr [edx],0FFFFFFFEh
00401312 33 C0 xor eax,eax
00401314 8B 5A 08 mov ebx,dword ptr [edx+8]
Observo que el hardware x86 puede reordenar estas instrucciones particulares de todos modos ya que no se refieren a la misma ubicación de memoria, y las lecturas pueden pasar las escrituras; Para solucionar realmente este ejemplo, necesitaría algún tipo de barrera de memoria. Volviendo a mi comentario anterior, si ambas fueron escrituras, el x86 no las volverá a ordenar, y el orden de escritura se verá en ese orden en otros subprocesos. Entonces, en ese caso, no creo que necesite una barrera de memoria, solo un pedido forzado.
No he visto al compilador reordenar dos escrituras (todavía) pero no he estado buscando muy bien (todavía); Acabo de tropezar con esto. Y, por supuesto, con optimizaciones solo porque no lo ves en esta compilación no significa que no aparecerá en la siguiente.
Entonces, ¿cómo obligo al compilador a ordenar esto?
Entiendo que puedo declarar que las ranuras de memoria en la estructura son volátiles. Siguen siendo ubicaciones de almacenamiento independientes , por lo que no veo cómo esto evita una optimización. Tal vez estoy malinterpretando lo que significa volátil?
EDITAR (20 de octubre): Gracias a todos los que respondieron. Mi implementación actual utiliza volatile (utilizada como la solución inicial), _ReadWriteBarrier (para marcar el código donde el compilador no debería reordenar), y algunos MemoryBarriers (donde ocurren lecturas y escrituras), y eso parece haber resuelto el problema .
EDIT: (2 de noviembre): Para estar limpio, terminé definiendo conjuntos de macros para ReadBarrier, WriteBarrier y ReadWriteBarrier. Existen conjuntos para el bloqueo previo y posterior, el desbloqueo previo y posterior y el uso general. Algunos de estos están vacíos, algunos contienen _ReadWriteBarrier y MemoryBarrier, según corresponda para los x86 y los típicos bloqueos de giro basados en XCHG [XCHG incluye un MemoryBarrier implícito, lo que evita esa necesidad en los pre / post ajustes de bloqueo). Luego los estacioné en el código en la documentación apropiada de los requisitos esenciales (no) de reordenación.
Ver _ReadWriteBarrier . Eso es un compilador intrínseco dedicado a lo que buscas. Asegúrese de verificar la documentación con su versión precisa de MSVC ("en desuso" en VS2012 ...). Tenga cuidado con la reordenación de la CPU (luego vea MemoryBarrier
La documentación states que los compiladores _ReadBarrier, _WriteBarrier y _ReadWriteBarrier (reordenación del compilador) y que la macro MemoryBarrier (reordenación de la CPU) están "en desuso" a partir de VS2012. Pero creo que continuarán trabajando bien durante algún tiempo ...
El nuevo código puede usar las nuevas instalaciones de C ++ 11 (enlaces en la página de MSDN)
Yo usaría la palabra clave volátil. Evitará que el compilador vuelva a ordenar las instrucciones. http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword