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.

questionAnswers(4)

yourAnswerToTheQuestion