Então me deparei com as postagens maravilhosas do blog de Jeff Preshing sobre o que é Acquire
/ Release
e como elas podem ser alcançadas com algumas barreiras de CPU .
Também li que SeqCst
se trata de uma ordem total que é garantidamente consistente com a relação não coerente-depois - embora às vezes possa contradizer a relação acontece-antes estabelecida por operações simples Acquire
devido Release
a razões históricas.
Minha pergunta é : como os antigos built-ins do GCC mapeiam no modelo de memória proposto pelo C++11 (e revisões posteriores)? Em particular, como mapear __sync_synchronize()
no C++11 ou no C/C++ moderno posterior?
No manual do GCC, essa chamada é simplesmente descrita como uma barreira de memória completa , que eu suponho ser a combinação de todos os quatro principais tipos de barreira, ou seja, barreirasLoadLoad
/ LoadStore
/ StoreLoad
/ todas de uma vez. Mas é equivalente a ? Ou talvez , formalmente falando , uma delas seja mais forte que a outra (o que eu suponho ser o caso aqui: em geral, uma cerca deve ser mais forte, pois requer que a cadeia de ferramentas/plataforma improvise uma ordenação global de alguma forma, não?), e acontece que a maioria das CPUs por aí fornece apenas instruções que satisfazem ambas (barreira de memória completa por , ordenação sequencial total por ) de uma vez , por exemplo x86 e PowerPC ?StoreStore
sync_synchronize
std::atomic_thread_fence(memory_order_seq_cst)
SeqCst
__sync_synchronize
std::atomic_thread_fence(memory_order_seq_cst)
mfence
hwsync
Ou __sync_synchronize
e std::atomic_thread_fence(memory_order_seq_cst)
são formalmente iguais ou são efetivamente iguais (ou seja, formalmente falando, são diferentes, mas nenhuma CPU comercializada se preocupa em diferenciar entre os dois), tecnicamente falando, uma memory_order_relaxed
carga no mesmo atômico ainda não pode ser confiável para sincronizar com /criar acontece antes da relação com ele, certo?
Tecnicamente falando, todas essas afirmações podem falhar, certo?
// Experiment 1, using C11 `atomic_thread_fence`: assertion is allowed to fail, right?
// global
static atomic_bool lock = false;
static atomic_bool critical_section = false;
// thread 1
atomic_store_explicit(&critical_section, true, memory_order_relaxed);
atomic_thread_fence(memory_order_seq_cst);
atomic_store_explicit(&lock, true, memory_order_relaxed);
// thread 2
if (atomic_load_explicit(&lock, memory_order_relaxed)) {
// We should really `memory_order_acquire` the `lock`
// or `atomic_thread_fence(memory_order_acquire)` here,
// or this assertion may fail, no?
assert(atomic_load_explicit(&critical_section, memory_order_relaxed));
}
// Experiment 2, using `SeqCst` directly on the atomic store
// global
static atomic_bool lock = false;
static atomic_bool critical_section = false;
// thread 1
atomic_store_explicit(&critical_section, true, memory_order_relaxed);
atomic_store_explicit(&lock, true, memory_order_seq_cst);
// thread 2
if (atomic_load_explicit(&lock, memory_order_relaxed)) {
// Again we should really `memory_order_acquire` the `lock`
// or `atomic_thread_fence(memory_order_acquire)` here,
// or this assertion may fail, no?
assert(atomic_load_explicit(&critical_section, memory_order_relaxed));
}
// Experiment 3, using GCC built-in: assertion is allowed to fail, right?
// global
static atomic_bool lock = false;
static atomic_bool critical_section = false;
// thread 1
atomic_store_explicit(&critical_section, true, memory_order_relaxed);
__sync_synchronize();
atomic_store_explicit(&lock, true, memory_order_relaxed);
// thread 2
if (atomic_load_explicit(&lock, memory_order_relaxed)) {
// we should somehow put a `LoadLoad` memory barrier here,
// or the assert might fail, no?
assert(atomic_load_explicit(&critical_section, memory_order_relaxed));
}
Eu tentei esses snippets no meu RPi 5, mas não vejo falhas nas asserções. Sim, isso não prova nada formalmente, mas também não esclarece a diferenciação entre __sync_synchronize
e std::atomic_thread_fence(memory_order_seq_cst)
.
Sim,
__sync_synchronize()
é pelo menos na prática equivalente astd::atomic_thread_fence(memory_order_seq_cst)
.Formalmente,
__sync_synchronize()
opera em termos de barreiras de memória e bloqueio de reordenação de memória, pois é anterior à existência do modelo de memória formal do C++11.atomic_thread_fence
opera em termos do modelo de memória do C++11; compilar para uma instrução de barreira completa é um detalhe de implementação.Então, por exemplo, não é exigido pelo padrão for para
thread_fence
fazer nada em um programa onde não hástd::atomic<>
objetos porque seu comportamento é definido apenas em termos de atômicos. Enquanto__sync_synchronize()
(ethread_fence
na prática como um detalhe de implementação em GCC/clang) poderia deixar você hackear algo em termos de sincronização emint
variáveis simples. Isso é UB em C++11, e uma má ideia mesmo em termos de uma implementação conhecida como GCC; veja Quem tem medo de um grande e mau compilador otimizador? re: a maldade óbvia vs. não óbvia (como cargas inventadas) que pode acontecer quando você apenas usa barreiras de memória em vez destd::atomic
comrelaxed
variáveis compartilhadas para impedir que um compilador as mantenha em registradores.Mas meu ponto é que, na prática, eles funcionam da mesma forma, mas são de modelos de memória diferentes: os
__sync
builtins são em termos de barreiras contra reordenação local de acessos à memória compartilhada coerente com cache (ou seja, uma visão de arquitetura de CPU), vs.std::atomic
coisas do C++11 sendo em termos de seu formalismo com ordens de modificação e sincronizações com/acontece antes. O que formalmente permite algumas coisas que não são plausíveis em uma CPU real que usa memória compartilhada coerente com cache.Sim, em seus blocos de código, a asserção pode falhar em uma CPU onde a reordenação LoadLoad é possível. Provavelmente não é possível com ambas as variáveis na mesma linha de cache. Veja o exemplo de problema de ordem de memória de variável atômica C++ não pode reproduzir a reordenação LoadStore para outro caso de tentativa de reproduzir a reordenação de memória.