C11 Atomic Adquirir / Liberar e x86_64 falta de coerência de carga / armazenamento?

10

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->readypara 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.

Chris Hall
fonte
5
No x86, todos os armazenamentos comuns (não não temporais) possuem semântica de lançamento. Intel 64 e IA-32 arquitecturas de software desenvolvedor manual Volume 3 (3A, 3B, 3C e 3D): Sistema Guia de Programação , 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.
EOF
Obrigado ! (Eu estava ficando maluco.) FWIW, eu recomendo o link - particularmente a seção 3, o "Modelo do Programador". Mas, para evitar o erro que cometi, observe que em "3.1 The Abstract Machine" existem "threads de hardware", cada um dos quais "um único fluxo de execução de instruções em ordem " (minha ênfase adicionada). Agora posso voltar a tentar entender o padrão C11 ... com menos dissonância cognitiva :-)
Chris Hall

Respostas:

1

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 parastdatomic .)

A @EOF comentou com a citação correta do manual da Intel:

Manual do desenvolvedor de software das arquiteturas Intel® 64 e IA-32 Volume 3 (3A, 3B, 3C e 3D): Guia de programação do sistema, 8.2.3.3 Os repositórios não são reordenados com cargas anteriores.


Nota de rodapé 1: ignorando lojas NT com ordem fraca; é por isso que você normalmente sfencedepois 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_sfenceantes 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_sfenceem outros casos ; geralmente, você só precisa bloquear a reordenação em tempo de compilação.

Peter Cordes
fonte
Acho que o x86-TSO: o modelo de um programador rigoroso e utilizável para multiprocessadores x86 é mais legível do que a descrição formal (relacionada) que você mencionou. Mas minha real ambição é entender completamente as seções 5.1.2.4 e 7.17.3 da norma C11 / C18. Em particular, acho que recebo Release / Adquirir / Adquirir + Liberar, mas o memory_order_seq_cst é definido separadamente e estou tendo dificuldades para ver como todos eles se encaixam :-(
Chris Hall
@ ChrisHall: Eu achei que ajudou a perceber exatamente o quão fraco o ACQ / rel pode ser, e para isso você precisa olhar para máquinas como POWER que podem fazer reordenações IRIW. (que seq-cst proíbe mas acq / rel não). Duas gravações atômicas em locais diferentes em threads diferentes sempre serão vistas na mesma ordem por outros threads? . Também como alcançar uma barreira StoreLoad em C ++ 11? tem alguma discussão sobre quão pouco o padrão garante formalmente sobre pedidos fora dos casos de sincronização com ou tudo seq-cst.
Peter Cordes
@ ChrisHall: A principal coisa que o seq-cst faz é bloquear a reordenação do StoreLoad. (No x86, é a única coisa que faz além do acq / rel). preshing.com/20120515/memory-reordering-caught-in-the-act usa asm, mas é equivalente a seq-cst vs. acq / rel
Peter Cordes