Estou lutando com a Seção 5.1.2.4 da Norma C11, em particular a semântica de Liberação / Aquisição. Observo que https://preshing.com/20120913/acquire-and-release-semantics/ (entre outros) afirma que:
... A semântica de liberação impede a reordenação de memória da liberação de gravação com qualquer operação de leitura ou gravação que a preceda na ordem do programa.
Então, para o seguinte:
typedef struct test_struct
{
_Atomic(bool) ready ;
int v1 ;
int v2 ;
} test_struct_t ;
extern void
test_init(test_struct_t* ts, int v1, int v2)
{
ts->v1 = v1 ;
ts->v2 = v2 ;
atomic_store_explicit(&ts->ready, false, memory_order_release) ;
}
extern int
test_thread_1(test_struct_t* ts, int v2)
{
int v1 ;
while (atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
ts->v2 = v2 ; // expect read to happen before store/release
v1 = ts->v1 ; // expect write to happen before store/release
atomic_store_explicit(&ts->ready, true, memory_order_release) ;
return v1 ;
}
extern int
test_thread_2(test_struct_t* ts, int v1)
{
int v2 ;
while (!atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
ts->v1 = v1 ;
v2 = ts->v2 ; // expect write to happen after store/release in thread "1"
atomic_store_explicit(&ts->ready, false, memory_order_release) ;
return v2 ;
}
onde esses são executados:
> in the "main" thread: test_struct_t ts ;
> test_init(&ts, 1, 2) ;
> start thread "2" which does: r2 = test_thread_2(&ts, 3) ;
> start thread "1" which does: r1 = test_thread_1(&ts, 4) ;
Portanto, eu esperaria que o segmento "1" tivesse r1 == 1 e o segmento "2" tivesse r2 = 4.
Eu esperaria isso porque (seguindo os parágrafos 16 e 18 da seção 5.1.2.4):
- todas as leituras e gravações (não atômicas) são "sequenciadas antes" e, portanto, "acontecem antes" da gravação / liberação atômica no encadeamento "1",
- qual "inter-thread-going-before" a leitura / aquisição atômica no thread "2" (quando lê 'true'),
- que por sua vez é "sequenciado antes" e, portanto, "acontece antes", o (não atômico) lê e grava (no segmento "2").
No entanto, é perfeitamente possível que eu não tenha entendido o padrão.
Observo que o código gerado para x86_64 inclui:
test_thread_1:
movzbl (%rdi),%eax -- atomic_load_explicit(&ts->ready, memory_order_acquire)
test $0x1,%al
jne <test_thread_1> -- while is true
mov %esi,0x8(%rdi) -- (W1) ts->v2 = v2
mov 0x4(%rdi),%eax -- (R1) v1 = ts->v1
movb $0x1,(%rdi) -- (X1) atomic_store_explicit(&ts->ready, true, memory_order_release)
retq
test_thread_2:
movzbl (%rdi),%eax -- atomic_load_explicit(&ts->ready, memory_order_acquire)
test $0x1,%al
je <test_thread_2> -- while is false
mov %esi,0x4(%rdi) -- (W2) ts->v1 = v1
mov 0x8(%rdi),%eax -- (R2) v2 = ts->v2
movb $0x0,(%rdi) -- (X2) atomic_store_explicit(&ts->ready, false, memory_order_release)
retq
E desde que R1 e X1 aconteçam nessa ordem, isso dá o resultado que eu espero.
Mas meu entendimento de x86_64 é que leituras acontecem em ordem com outras leituras e gravações acontecem em ordem com outras gravações, mas leituras e gravações podem não acontecer em ordem entre si. O que implica que é possível que X1 ocorra antes de R1, e mesmo que X1, X2, W2, R1 ocorram nessa ordem - acredito. [Isso parece desesperadamente improvável, mas se o R1 fosse retido por alguns problemas de cache?]
Por favor: o que não estou entendendo?
Observo que, se eu alterar as cargas / lojas de ts->ready
para memory_order_seq_cst
, o código gerado para as lojas será:
xchg %cl,(%rdi)
o que é consistente com meu entendimento de x86_64 e fornecerá o resultado esperado.
fonte
8.2.3.3 Stores Are Not Reordered With Earlier Loads
. Portanto, seu compilador está traduzindo corretamente seu código (que surpreendente), de modo que seu código seja efetivamente completamente seqüencial e nada interessante aconteça simultaneamente.Respostas:
O modelo de memória do x86 é basicamente consistência seqüencial mais um buffer de armazenamento (com encaminhamento de armazenamento). Portanto, toda loja é uma loja de lançamento 1 . É por isso que apenas as lojas seq-cst precisam de instruções especiais. ( Mapeamentos atômicos C / C ++ 11 para asm ). Além disso, https://stackoverflow.com/tags/x86/info possui alguns links para documentos x86, incluindo uma descrição formal do modelo de memória x86-TSO (basicamente ilegível para a maioria dos humanos; requer percorrer muitas definições).
Como você já está lendo a excelente série de artigos de Jeff Preshing, mostrarei outro que entra em mais detalhes: https://preshing.com/20120930/weak-vs-strong-memory-models/
A única reordenação permitida no x86 é StoreLoad, não LoadStore , se estivermos falando nesses termos. (O encaminhamento de loja pode fazer coisas divertidas extras se uma carga se sobrepuser parcialmente a uma loja; instruções de carregamento globalmente invisíveis , embora você nunca consiga isso no código gerado pelo compilador para
stdatomic
.)A @EOF comentou com a citação correta do manual da Intel:
Nota de rodapé 1: ignorando lojas NT com ordem fraca; é por isso que você normalmente
sfence
depois de fazer armazenamentos no NT. As implementações do C11 / C ++ 11 assumem que você não está usando repositórios do NT. Se estiver, use_mm_sfence
antes de uma operação de liberação para garantir que ela respeite seus repositórios do NT. (Em geral , não use_mm_mfence
/_mm_sfence
em outros casos ; geralmente, você só precisa bloquear a reordenação em tempo de compilação.fonte