variáveis voláteis e operações atômicas no Visual C++ x86
-
12-11-2019 - |
Pergunta
O carregamento simples possui semântica de aquisição em x86, o armazenamento simples possui semântica de liberação, porém o compilador ainda pode reordenar instruções.Embora barreiras e instruções bloqueadas (xchg bloqueado, cmpxchg bloqueado) impeçam o reordenamento do hardware e do compilador, cargas e armazenamentos simples ainda são necessários para proteção com barreiras do compilador.Visual C++ fornece a função _ReadWriterBarrier(), que evita a reordenação do compilador, e C++ também fornece palavras-chave voláteis pelo mesmo motivo.Escrevo todas essas informações apenas para ter certeza de que entendi tudo certo.Portanto, tudo o que foi escrito acima é verdade. Existe algum motivo para marcar como variáveis voláteis que serão usadas em funções protegidas com _ReadWriteBarrier()?
Por exemplo:
int load(int& var)
{
_ReadWriteBarrier();
T value = var;
_ReadWriteBarrier();
return value;
}
É seguro tornar essa variável não volátil?Pelo que entendi, porque a função está protegida e nenhuma reordenação pode ser feita pelo compilador interno.Por outro lado, o Visual C++ fornece um comportamento especial para variáveis voláteis (diferente daquele padrão), faz leituras e gravações voláteis, cargas e armazenamentos atômicos, mas meu alvo é x86 e cargas e armazenamentos simples devem ser atômicos em x86 de qualquer maneira, certo?
Desde já, obrigado.
Solução
A palavra-chave volátil também está disponível em C."volátil" é frequentemente usado em sistemas embarcados, especialmente quando o valor da variável pode mudar a qualquer momento - sem que nenhuma ação seja tomada pelo código - três cenários comuns incluem a leitura de um registro periférico mapeado na memória ou variáveis globais modificadas por um interromper a rotina de serviço ou aqueles dentro de um programa multithread.
Portanto, é o último cenário em que volátil pode ser considerado semelhante a _ReadWriteBarrier.
_ReadWriteBarrier não é uma função - _ReadWriteBarrier não insere nenhuma instrução adicional e não impede que a CPU reorganize leituras e gravações - apenas evita que o compilador as reorganize._ReadWriteBarrier evita a reordenação do compilador.
MemoryBarrier evita a reordenação da CPU!
Um compilador normalmente reorganiza instruções...C++ não contém suporte integrado para programas multithread, portanto, o compilador assume que o código é de thread único ao reordenar o código.Com o MSVC, use _ReadWriteBarrier no código, para que o compilador não mova leituras e gravações nele.
Verifique este link para uma discussão mais detalhada sobre esses tópicoshttp://msdn.microsoft.com/en-us/library/ee418650(v=vs.85).aspx
Em relação ao seu trecho de código - você não precisa usar ReadWriteBarrier como uma primitiva SYNC - a primeira chamada para _ReadWriteBarrier não é necessária.
Ao usar ReadWriteBarrier você não precisa usar volátil
Você escreveu "faz leituras e gravações voláteis de cargas e armazenamentos atômicos" - não acho que seja correto dizer isso, atomicidade e volatilidade são diferentes.As operações atômicas são consideradas indivisíveis - ... http://www.yoda.arachsys.com/csharp/threads/volatility.shtml
Outras dicas
Observação:Não sou especialista neste assunto, algumas de minhas afirmações são "o que ouvi na internet", mas acho que ainda posso esclarecer alguns equívocos.
[editar] Em geral, eu confiaria nas especificidades da plataforma, como leituras atômicas x86 e falta de OOOX, apenas em otimizações locais isoladas que são protegidas por um #ifdef
verificando a plataforma alvo, idealmente acompanhada por uma solução portátil no #else
caminho.
Coisas a observar
- atomicidade das operações de leitura/gravação
- reordenação devido a otimizações do compilador (isso inclui uma ordem diferente vista por outro thread devido ao simples cache de registro)
- execução fora de ordem na CPU
Possíveis equívocos
1. Pelo que entendi, porque a função está protegida e nenhuma reordenação pode ser feita pelo compilador interno.
[editar] Esclarecer:o _ReadWriteBarrier
fornece proteção contra reordenação de instruções; no entanto, você precisa olhar além do escopo da função. _ReadWriteBarrier
foi corrigido no VS 2010 para fazer isso, versões anteriores podem ser quebradas (dependendo das otimizações que elas realmente fazem).
A otimização não se limita às funções.Existem vários mecanismos (inlining automático, geração de código de tempo de link) que abrangem funções e até unidades de compilação (e podem fornecer otimizações muito mais significativas do que o cache de registro de escopo pequeno).
2. Visual C++ [...] faz leituras e gravações voláteis, cargas atômicas e armazenamentos,
Onde você encontrou aquilo? MSDN diz que além do padrão, colocará barreiras de memória em torno de leituras e gravações, sem garantia para leituras atômicas.
[editar] Observe que C#, Java, Delphi etc.têm modelos de memória diferentes e podem oferecer garantias diferentes.
3. cargas e armazenamentos simples deveriam ser atômicos em x86 de qualquer maneira, certo?
Não, eles não são.Leituras desalinhadas não são atômicas.Eles acontecer de ser atômico se estiverem bem alinhados - um fato no qual não confiaria, a menos que seja isolado e facilmente trocado.Caso contrário, sua "simplificação para x86" se tornará um bloqueio para esse alvo.
[editar] Leituras desalinhadas acontecem:
char * c = new char[sizeof(int)+1];
load(*(int *)c); // allowed by standard to be unaligned
load(*(int *)(c+1)); // unaligned with most allocators
#pragma pack(push,1)
struct
{
char c;
int i;
} foo;
load(foo.i); // caller said so
#pragma pack(pop)
É claro que tudo isso é acadêmico, se você lembrar que o parâmetro deve estar alinhado e você controla todo o código.Eu não escreveria mais esse código, porque muitas vezes fui mordido pela preguiça do passado.
4. O carregamento simples possui semântica de aquisição em x86, o armazenamento simples possui semântica de liberação
Não.Os processadores x86 não usam execução fora de ordem (ou melhor, nenhum OOOX visível - eu acho), mas isso não impede o otimizador de reordenar as instruções.
5. _ReadBarrier / _writeBarrier / _readWriteBarrier Faça toda a mágica que não - eles apenas impedem a reordenação pelo otimizador.O MSDN finalmente tornou isso um grande aviso ruim para VS2010, mas a informação aparentemente se aplica a versões anteriores também.
Agora, para sua pergunta.
Presumo que o objetivo do snippet seja passar qualquer variável N e carregá-la (atomicamente?) A escolha direta seria uma leitura interligada ou (no Visual C++ 2005 e posterior) uma leitura volátil.
Caso contrário, você precisaria de uma barreira para o compilador e para a CPU antes da leitura. No salão VC++, isso seria:
int load(int& var)
{
// force Optimizer to complete all memory writes:
// (Note that this had issues before VC++ 2010)
_WriteBarrier();
// force CPU to settle all pending read/writes, and not to start new ones:
MemoryBarrier();
// now, read.
int value = var;
return value;
}
Não, isso _WriteBarrier
tem um segundo aviso no MSDN:*Nas versões anteriores do compilador Visual C++, as funções _ReadWriteBarrier e _WriteBarrier eram aplicadas apenas localmente e não afetavam as funções na árvore de chamadas.Essas funções agora são aplicadas em toda a árvore de chamadas.*
EU ter esperança está correto.stackoverflowers, corrija-me se eu estiver errado.