Considere a definição de compare_and_exchange_strong_explicit
:
_Bool atomic_compare_exchange_strong_explicit( volatile A* obj,
C* expected, C desired,
memory_order succ,
memory_order fail );
Estou confuso com o caso de uso do memory_order fail
. Considerando o x86, está perfeitamente claro que lock cmpxchg
pode falhar, mas também está claramente definido que (enfatize o meu)
Leituras ou gravações não podem ser reordenadas com instruções de E/S, instruções bloqueadas ou instruções de serialização
o que torna o memory_order fail
tipo irrelevante, pois lock
garante consistência sequencial em qualquer caso.
Exemplo :
#include <stdatomic.h>
void fail_seqcst(volatile int *p, int *expected, int *desirable){
atomic_compare_exchange_strong_explicit(p, expected, desirable, memory_order_release, memory_order_seq_cst);
}
void fail_relaxed(volatile int *p, int *expected, int *desirable){
atomic_compare_exchange_strong_explicit(p, expected, desirable, memory_order_release, memory_order_relaxed);
}
Compila para :
fail_relaxed:
mov ecx, edx
mov eax, DWORD PTR [rsi]
lock
cmpxchg DWORD PTR [rdi], ecx
mov DWORD PTR [rsi], eax
sete al
movzx eax, al
ret
fail_seqcst:
mov ecx, edx
mov eax, DWORD PTR [rsi]
lock
cmpxchg DWORD PTR [rdi], ecx
mov DWORD PTR [rsi], eax
sete al
movzx eax, al
ret
Existe alguma otimização que o compilador possa fazer que diferencie o código memory_order_relaxed
e, memory_order_seq_cst
nesse caso, em x86
? Ou talvez exista uma arquitetura que possa tornar essa diferença significativa?
A ordem de falha só é relevante em ASM para não-x86, onde muitas vezes são necessárias instruções de barreira separadas, que podem ser colocadas apenas no caminho de execução para o caso de sucesso.
Ele pode permitir a reordenação em tempo de compilação, embora os compiladores x86 atuais possam não fazer isso. No código que se ramifica em uma tentativa de CAS, uma carga relaxada (ou leitura não atômica) antes do CAS poderia ser movida para o caminho de falha se seu resultado fosse usado apenas lá. Mas somente se a ordem de falha do CAS não for uma operação de liberação, apenas aquisição ou relaxamento. (
.load(acquire)
ou mais forte não pode reordenar desta forma, pois isso aconteceria - antes do caminho de sucesso também, e o próprio CAS antes talvez excluindo alguns valores.)Todas as opções possíveis de
memory_order
CAS_weak e CAS_strong geram o mesmo asm x86 queatomic_compare_exchanges_strong(ptr, &expected, desired, seq_cst)
, que élock cmpxchg
.Se tiver sucesso ou falhar,
lock cmpxchg
é uma barreira completa comoatomic_thread_fence(seq_cst)
(ao contrário de um carregamento, armazenamento ou RMW seq_cst em um ISA com ordem fraca, como AArch64, onde SC RMW não impediria que uma loja relaxada posterior reordenasse com uma loja relaxada anterior, por exemplo o último pode reordenar com o lado de armazenamento do RMW e o anterior pode reordenar com o lado de carga do RMW). As operações são diferentes das cercas no ISO C++ e em alguns ISAs reais. Portanto, é muito mais forte do que o ISO C++ exige para uma operação seq_cst, tão forte quanto uma cerca completa.E o x86
lock cmpxchg
até suja a linha de cache em caso de falha (novamente ao contrário de uma implementação LL/SC , que se ramifica na tentativa de armazenamento se a comparação for falsa). Embora até mesmo o LL possa tentar colocar a linha de cache no estado de propriedade exclusiva, para que possa não será muito melhor para a contenção, mesmo que evite uma resposta posterior. Não tenho certeza de como isso funciona.No que diz respeito à CPU,
lock cmpxchg
são apenas ALU e configuração de sinalizadores diferentes versus operações sempre bem-sucedidas, comolock add
ouxchg
. Todas essas são barreiras completas e, na verdade, um manequimlock or dword [rsp], 0
ou algo semelhante é como a maioria dos compiladores implementaatomic_thread_fence(seq_cst)
porque é mais rápido que omfence
.A Intel até documenta que está sempre armazenando para que o processador nunca produza uma leitura bloqueada sem também produzir uma gravação bloqueada, embora a mesma frase fale sobre "a interface com o barramento do processador". As operações ed alinhadas
lock
em regiões de memória normais (armazenáveis em cache) apenas recebem um bloqueio de cache (atrasando a resposta MESI), e não qualquer coisa que seja realmente visível em um barramento fora do núcleo da CPU. Então essa provavelmente não é mais uma documentação realmente significativa, é mais uma coisa como se desde o P6 (Pentium Pro) em 1995, pelo menos.