As leituras voláteis, mas não protegidas, podem produzir valores obsoletos indefinidamente? (em hardware real)
Ao responderessa questão outra questão sobre a situação do OP surgiu e eu estava inseguro: é principalmente uma questão de arquitetura de processador, mas também com uma dúvida sobre o modelo de memória do C ++ 11.
Basicamente, o código do OP estava circulando infinitamente em níveis de otimização mais altos por causa do código a seguir (ligeiramente modificado para simplificar):
while (true) {
uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
if (ov & MASK) {
continue;
}
if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
break;
}
}
Onde__sync_val_compare_and_swap()
é o CAS atômico do GCC embutido. GCC (razoavelmente) otimizou isso em um loop infinito no caso em quebits_ & mask
foi detectado para sertrue
antes de entrar no loop, ignorando totalmente a operação do CAS, sugeri a seguinte alteração (que funciona):
while (true) {
uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
if (ov & MASK) {
__sync_synchronize();
continue;
}
if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
break;
}
}
Depois que eu respondi, OP notou que mudarbits_
paravolatile uint8_t
parece funcionar também. Eu sugeri não seguir esse caminho, já quevolatile
normalmente não deve ser usado para sincronização, e não parece haver muita desvantagem em usar uma cerca aqui de qualquer maneira.
No entanto, eu pensei sobre isso mais, e neste caso, a semântica é tal que não importa realmente se oov & MASK
A verificação é baseada em um valor obsoleto, desde que não seja baseada em um valor indefinidamente obsoleto (ou seja, desde que o loop seja quebrado eventualmente), uma vez que a tentativa real de atualizaçãobits_
está sincronizado. Então évolatile
o suficiente aqui para garantir que este loop termina eventualmente sebits_
é atualizado por outro segmento tal quebits_ & MASK == false
, para qualquer processador existente? Em outras palavras, na ausência de uma cerca de memória explícita, é praticamente possível que leituras não otimizadas pelo compilador sejam efetivamente otimizadas pelo processador, indefinidamente? (EDITAR: Para ser claro, estou perguntando aqui o que o hardware moderno pode realmente fazer, supondo que as leituras sejam emitidas em um loop pelo compilador, portanto, não é tecnicamente uma questão de linguagem, embora seja conveniente expressá-la em termos de semântica em C ++.
Esse é o ângulo do hardware, mas para atualizá-lo levemente e torná-lo também uma questão respondível sobre o modelo de memória C ++ 11, considere a seguinte variação para o código acima:
// bits_ is "std::atomic<unsigned char>"
unsigned char ov = bits_.load(std::memory_order_relaxed);
while (true) {
if (ov & MASK) {
ov = bits_.load(std::memory_order_relaxed);
continue;
}
// compare_exchange_weak also updates ov if the exchange fails
if (bits_.compare_exchange_weak(ov, ov | MASK, std::memory_order_acq_rel)) {
break;
}
}
cppreferência afirma questd::memory_order_relaxed
implica "sem restrições na reordenação de acessos à memória em torno da variável atômica", de forma independente do que o hardware real irá ou não fazer, implica quebits_.load(std::memory_order_relaxed)
poderia tecnicamenteNunca leia um valor atualizado depoisbits_
é atualizado em outro thread em uma implementação em conformidade?
EDITAR: Achei isso no padrão (29.4 p13):
As implementações devem tornar as lojas atômicas visíveis para cargas atômicas dentro de um período de tempo razoável.
Então, aparentemente, esperar "infinitamente longo" por um valor atualizado é (principalmente?) Fora de questão, mas não há garantia de qualquer intervalo de tempo específico de frescura que não seja deve ser "razoável"; Ainda assim, a questão sobre o comportamento real do hardware permanece.