Eu entendo que volatile
informa ao compilador que o valor pode ser alterado, mas para realizar essa funcionalidade, o compilador precisa introduzir um limite de memória para fazê-lo funcionar?
Do meu entendimento, a sequência de operações em objetos voláteis não pode ser reordenada e deve ser preservada. Isso parece implicar que algumas barreiras de memória são necessárias e que não há realmente uma maneira de contornar isso. Estou correto em dizer isso?
Há uma discussão interessante nesta questão relacionada
... Acessos a variáveis voláteis distintas não podem ser reordenados pelo compilador, desde que ocorram em expressões completas separadas ... certo que volátil é inútil para segurança de thread, mas não pelas razões que ele deu. Não é porque o compilador pode reordenar os acessos a objetos voláteis, mas porque a CPU pode reordená-los. Operações atômicas e barreiras de memória evitam que o compilador e a CPU reordenem
Ao que David Schwartz responde nos comentários :
... Não há diferença, do ponto de vista do padrão C ++, entre o compilador fazer algo e o compilador emitir instruções que fazem com que o hardware faça algo. Se a CPU pode reordenar os acessos aos voláteis, o padrão não exige que sua ordem seja preservada. ...
... O padrão C ++ não faz nenhuma distinção sobre o que faz o reordenamento. E você não pode argumentar que a CPU pode reordená-los sem nenhum efeito observável, então tudo bem - o padrão C ++ define sua ordem como observável. Um compilador é compatível com o padrão C ++ em uma plataforma se ele gerar código que faça a plataforma fazer o que o padrão exige. Se o padrão requer que os acessos aos voláteis não sejam reordenados, então a plataforma que os reordena não é compatível. ...
Meu ponto é que se o padrão C ++ proíbe o compilador de reordenar acessos a voláteis distintos, na teoria de que a ordem de tais acessos faz parte do comportamento observável do programa, então também requer que o compilador emita código que proíbe a CPU de fazer então. O padrão não diferencia entre o que o compilador faz e o que o código gerado pelo compilador faz com que a CPU faça.
O que resulta em duas perguntas: algum deles está "certo"? O que as implementações reais realmente fazem?
fonte
volatile
variáveis sejam otimizadas pelos caches da CPU. Ou todos esses compiladores não estão em conformidade ou o padrão não significa o que você pensa que significa. (O padrão não distingue entre o que o compilador faz e o que o compilador faz a CPU fazer. O trabalho do compilador é emitir código que, quando executado, está em conformidade com o padrão.)Respostas:
Em vez de explicar o que
volatile
significa, permita-me explicar quando você deve usarvolatile
.volatile
variável é praticamente a única coisa que o padrão permite que você faça de dentro de um manipulador de sinal. Desde C ++ 11 você pode usarstd::atomic
para esse propósito, mas apenas se o atômico não tiver bloqueio.setjmp
acordo com a Intel .Por exemplo:
volatile int *foo = some_memory_mapped_device; while (*foo) ; // wait until *foo turns false
Sem o
volatile
especificador, o compilador pode otimizar completamente o loop. Ovolatile
especificador diz ao compilador que ele não pode presumir que 2 leituras subsequentes retornem o mesmo valor.Observe que
volatile
não tem nada a ver com threads. O exemplo acima não funcionará se houver um thread diferente gravando*foo
porque não há operação de aquisição envolvida.Em todos os outros casos, o uso de
volatile
deve ser considerado não portátil e não mais passar na revisão de código, exceto ao lidar com compiladores pré-C ++ 11 e extensões de compilador (como a/volatile:ms
chave msvc , que é habilitada por padrão no X86 / I64).fonte
setjmp
são as duas garantias que o padrão faz. Por outro lado, a intenção , pelo menos no início, era oferecer suporte a IO mapeado em memória. O que em alguns processadores pode exigir uma cerca ou um membar.volatile
acessos.Um compilador C ++ que está em conformidade com a especificação não é necessário para introduzir um limite de memória. Seu compilador particular pode; encaminhe sua pergunta para os autores do seu compilador.
A função de "volatile" em C ++ não tem nada a ver com threading. Lembre-se de que o objetivo de "volátil" é desabilitar as otimizações do compilador para que a leitura de um registro que está mudando devido a condições exógenas não seja otimizada. Um endereço de memória que está sendo escrito por um thread diferente em uma CPU diferente é um registro que está mudando devido a condições exógenas? Não. Novamente, se alguns autores do compilador escolheram tratar endereços de memória sendo gravados por threads diferentes em CPUs diferentes como se fossem registros que mudam devido a condições exógenas, esse é o problema; eles não são obrigados a fazê-lo. Nem são necessárias - mesmo que introduz uma cerca memória - para, por exemplo, garantir que todos os fios vê uma consistente ordenação de leituras e gravações voláteis.
Na verdade, volatile é praticamente inútil para threading em C / C ++. A melhor prática é evitá-lo.
Além disso: as barreiras de memória são um detalhe de implementação de arquiteturas de processador específicas. Em C #, onde volatile é explicitamente projetado para multithreading, a especificação não diz que meias fences serão introduzidas, porque o programa pode estar sendo executado em uma arquitetura que não tem fences em primeiro lugar. Em vez disso, novamente, a especificação oferece certas garantias (extremamente fracas) sobre quais otimizações serão evitadas pelo compilador, tempo de execução e CPU para colocar certas restrições (extremamente fracas) em como alguns efeitos colaterais serão ordenados. Na prática, essas otimizações são eliminadas pelo uso de meias barreiras, mas esse é um detalhe de implementação sujeito a alterações no futuro.
O fato de que você se preocupa com a semântica de volátil em qualquer idioma no que diz respeito ao multithreading indica que você está pensando em compartilhar memória entre threads. Considere simplesmente não fazer isso. Isso torna seu programa muito mais difícil de entender e muito mais provável de conter bugs sutis e impossíveis de reproduzir.
fonte
volatile
foi introduzido no padrão C. Ainda assim, como o padrão não pode especificar coisas como o que realmente acontece em um "acesso", ele diz que "O que constitui um acesso a um objeto que tem um tipo qualificado por volátil é definido pela implementação." Muitas implementações hoje não fornecem uma definição útil de um acesso, o que IMHO viola o espírito do padrão, mesmo que esteja em conformidade com a letra.volatile
semânticas são mais fortes do que isso, o compilador tem que gerar todos os acessos solicitados (1.9 / 8, 1.9 / 12), não simplesmente garantir que mudanças exógenas sejam eventualmente detectadas (1.10 / 27). No mundo da E / S mapeada por memória, uma leitura de memória pode ter uma lógica arbitrária associada, como um getter de propriedade. Você não otimizaria chamadas para getters de propriedade de acordo com as regras que você definiuvolatile
, nem o padrão permite.O que David está esquecendo é o fato de que o padrão C ++ especifica o comportamento de vários threads interagindo apenas em situações específicas e todo o resto resulta em comportamento indefinido. Uma condição de corrida envolvendo pelo menos uma gravação é indefinida se você não usar variáveis atômicas.
Consequentemente, o compilador tem todo o direito de renunciar a qualquer instrução de sincronização, já que sua CPU só notará a diferença em um programa que exibe um comportamento indefinido devido à falta de sincronização.
fonte
volatile
não tem nada a ver com tópicos; seu propósito original era oferecer suporte a IO mapeado em memória. E, pelo menos em alguns processadores, o suporte de IO mapeado em memória exigiria barreiras. (Compiladores não fazem isso, mas esse é um problema diferente.)volatile
tem muito a ver com threads:volatile
lida com a memória que pode ser acessada sem que o compilador saiba que pode ser acessada, e cobre muitos usos do mundo real de dados compartilhados entre threads em uma CPU específica.Em primeiro lugar, os padrões C ++ não garantem as barreiras de memória necessárias para ordenar adequadamente as leituras / gravações que não são atômicas. variáveis voláteis são recomendadas para uso com MMIO, tratamento de sinais, etc. Na maioria das implementações, volatile não é útil para multi-threading e geralmente não é recomendado.
Em relação à implementação de acessos voláteis, esta é a escolha do compilador.
Este artigo , que descreve o comportamento do gcc , mostra que você não pode usar um objeto volátil como uma barreira de memória para ordenar uma sequência de gravações na memória volátil.
Com relação ao comportamento do icc , achei esta fonte dizendo também que volátil não garante pedidos de acesso à memória.
O compilador Microsoft VS2013 tem um comportamento diferente. Esta documentação explica como volatile reforça a semântica Release / Acquire e permite que objetos voláteis sejam usados em bloqueios / releases em aplicativos multithread.
Outro aspecto que precisa ser levado em consideração é que o mesmo compilador pode ter um comportamento diferente. a volátil, dependendo da arquitetura de hardware de destino . Este post sobre o compilador MSVS 2013 afirma claramente as especificações da compilação com volátil para plataformas ARM.
Então, minha resposta para:
seria: Não garantido, provavelmente não, mas alguns compiladores podem fazer isso. Você não deve confiar no fato de que sim.
fonte
volatile
impede o compilador de reordenar cargas / armazenamentos? Ou você está dizendo que o padrão C ++ exige isso? E se for o último, você pode responder ao meu argumento em contrário citado na pergunta original?volatile
lvalue. Como deixa a definição de "acesso" para a implementação, no entanto, isso não nos compra muito se a implementação não se importar.volatile
, mas não há nenhum fence no código gerado pelo compilador no Visual Studios 2012.volatile
é aquele especificamente enumerado pelo padrão. (setjmp
, sinais e assim por diante.)O compilador apenas insere um limite de memória na arquitetura Itanium, até onde eu sei.
A
volatile
palavra-chave é realmente melhor usada para mudanças assíncronas, por exemplo, manipuladores de sinal e registros mapeados na memória; geralmente é a ferramenta errada para usar em programação multithread.fonte
volatile
é extremamente útil para muitos usos que nunca lidam com hardware. Sempre que desejar que a implementação gere um código de CPU que siga de perto o código C / C ++, usevolatile
.Depende de qual compilador é "o compilador". O Visual C ++ faz isso, desde 2005. Mas o padrão não exige isso, portanto, alguns outros compiladores não.
fonte
int volatile i; int main() { return i; }
gera um principal com exatamente duas instruções:mov eax, i; ret 0;
.cl /help
diz a versão 18.00.21005.1. O diretório em que está éC:\Program Files (x86)\Microsoft Visual Studio 12.0\VC
. O cabeçalho da janela de comando diz VS 2013. Então, com relação à versão ... As únicas opções que usei foram/c /O2 /Fa
. (Sem o/O2
, ele também configura o frame da pilha local. Mas ainda não há instrução de fence.)main
para que o compilador pudesse ver todo o programa e saber que não havia outras threads, ou pelo menos nenhum outro acesso à variável antes da minha (portanto, não poderia haver problemas de cache) poderia afetar isso também, mas de alguma forma, eu duvido.Isso é em grande parte da memória e baseado no pré-C ++ 11, sem threads. Mas tendo participado das discussões sobre threading no comitê, posso dizer que nunca houve uma intenção do comitê que
volatile
pudesse ser usada para sincronização entre threads. A Microsoft propôs, mas a proposta não foi aceita.A principal especificação de
volatile
é que o acesso a um volátil representa um "comportamento observável", assim como o IO. Da mesma forma, o compilador não pode reordenar ou remover IO específico, ele não pode reordenar ou remover acessos a um objeto volátil (ou mais corretamente, acessos por meio de uma expressão lvalue com tipo qualificado volátil). A intenção original do volátil era, na verdade, oferecer suporte a IO mapeado em memória. O "problema" com isso, no entanto, é que é a implementação definida o que constitui um "acesso volátil". E muitos compiladores o implementam como se a definição fosse "uma instrução que lê ou grava na memória foi executada". Que é uma definição legal, embora inútil, se a implementação especificar. (Ainda não encontrei a especificação real de nenhum compilador.Indiscutivelmente (e é um argumento que eu aceito), isso viola a intenção do padrão, uma vez que a menos que o hardware reconheça os endereços como IO mapeado na memória e iniba qualquer reordenamento, etc., você não pode nem usar volátil para IO mapeado na memória, pelo menos nas arquiteturas Sparc ou Intel. No entanto, nenhum dos comilers que examinei (Sun CC, g ++ e MSC) produz qualquer instrução fence ou membar. (Na época em que a Microsoft propôs estender as regras
volatile
, acho que alguns de seus compiladores implementaram sua proposta e emitiram instruções de fence para acessos voláteis. Não verifiquei o que os compiladores recentes fazem, mas não me surpreenderia se dependesse em alguma opção de compilador. A versão que verifiquei - acho que era VS6.0 - não emitia fences, no entanto.fonte
volatile
semântica é perfeitamente adequada. Geralmente, esses periféricos relatam suas áreas de memória como não armazenáveis em cache, o que ajuda no reordenamento no nível do hardware.ASI_REAL_IO
parte do espaço de endereço, acho que você deve estar bem. (Altera NIOS usa uma técnica semelhante, com bits altos do endereço controlando o desvio de MMU; tenho certeza que há outros também)Não precisa. Volátil não é um primitivo de sincronização. Ele apenas desativa as otimizações, ou seja, você obtém uma sequência previsível de leituras e gravações dentro de um thread na mesma ordem prescrita pela máquina abstrata. Mas ler e escrever em threads diferentes não têm ordem em primeiro lugar, não faz sentido falar em preservar ou não preservar sua ordem. A ordem entre os theads pode ser estabelecida por primitivos de sincronização, você obtém UB sem eles.
Um pouco de explicação sobre as barreiras de memória. Uma CPU típica possui vários níveis de acesso à memória. Há um pipeline de memória, vários níveis de cache, RAM etc.
As instruções do Membar liberam o pipeline. Eles não alteram a ordem em que as leituras e escritas são executadas, apenas força a execução das mais importantes em um determinado momento. É útil para programas multithread, mas não muito em contrário.
Cache (s) normalmente são automaticamente coerentes entre CPUs. Se quisermos ter certeza de que o cache está sincronizado com a RAM, é necessário liberar o cache. É muito diferente de um membar.
fonte
volatile
apenas desativa as otimizações do compilador? Isso não faz sentido. Qualquer otimização que o compilador pode fazer pode, pelo menos em princípio, ser igualmente bem feita pela CPU. Portanto, se o padrão diz que apenas desabilita as otimizações do compilador, isso significa que não fornecerá nenhum comportamento em que se possa confiar em código portátil. Mas isso obviamente não é verdade porque o código portátil pode confiar em seu comportamento em relação aossetjmp
sinais e.O compilador precisa introduzir uma barreira de memória em torno dos
volatile
acessos se, e somente se, isso for necessário para fazer os usosvolatile
especificados no trabalho padrão (setjmp
manipuladores de sinal e assim por diante) naquela plataforma particular.Observe que alguns compiladores vão muito além do que é exigido pelo padrão C ++ para torná
volatile
-los mais poderosos ou úteis nessas plataformas. O código portátil não deve dependervolatile
de nada além do que está especificado no padrão C ++.fonte
Eu sempre uso volátil em rotinas de serviço de interrupção, por exemplo, o ISR (frequentemente código de montagem) modifica alguma localização de memória e o código de nível superior que roda fora do contexto de interrupção acessa a localização de memória através de um ponteiro para volátil.
Eu faço isso para RAM, bem como para E / S mapeada em memória.
Com base na discussão aqui, parece que este ainda é um uso válido de volátil, mas não tem nada a ver com vários threads ou CPUs. Se o compilador de um microcontrolador "sabe" que não pode haver nenhum outro acesso (por exemplo, tudo está no chip, sem cache e há apenas um núcleo), eu pensaria que uma barreira de memória não está implícita de forma alguma, o compilador só precisa evitar certas otimizações.
À medida que empilhamos mais coisas no "sistema" que executa o código-objeto, quase todas as apostas são canceladas, pelo menos foi assim que li esta discussão. Como um compilador poderia cobrir todas as bases?
fonte
Eu acho que a confusão em torno do reordenamento volátil e de instrução decorre das 2 noções de reordenamento que as CPUs fazem:
Volátil afeta como um compilador gera o código assumindo a execução de thread único (isso inclui interrupções). Isso não implica nada sobre instruções de barreira de memória, mas impede que um compilador execute certos tipos de otimizações relacionadas a acessos de memória.
Um exemplo típico é recuperar um valor da memória, em vez de usar um armazenado em cache em um registro.
Execução fora de ordem
As CPUs podem executar instruções fora de ordem / especulativamente, desde que o resultado final possa ter acontecido no código original. As CPUs podem realizar transformações que não são permitidas em compiladores porque os compiladores só podem realizar transformações corretas em todas as circunstâncias. Em contraste, as CPUs podem verificar a validade dessas otimizações e retirá-las caso se revelem incorretas.
Sequência de leitura / gravação de memória vista por outras CPUs
O resultado final de uma seqüência de instruções, a ordem efetiva, deve estar de acordo com a semântica do código gerado por um compilador. No entanto, a ordem de execução real escolhida pela CPU pode ser diferente. A ordem efetiva vista em outras CPUs (cada CPU pode ter uma visão diferente) pode ser restringida por barreiras de memória.
Não tenho certeza do quanto a ordem efetiva e a real podem diferir, porque não sei até que ponto as barreiras de memória podem impedir que as CPUs realizem uma execução fora de ordem.
Fontes:
fonte
Enquanto eu trabalhava em um tutorial em vídeo para download on-line para desenvolvimento de gráficos 3D e mecanismo de jogo trabalhando com OpenGL moderno. Nós usamos
volatile
em uma de nossas classes. O site do tutorial pode ser encontrado aqui e o vídeo que trabalha com avolatile
palavra-chave é encontrado noShader Engine
vídeo da série 98. Esses trabalhos não são meus, mas são credenciadosMarek A. Krzeminski, MASc
e este é um trecho da página de download do vídeo.E se você estiver inscrito no site dele e tiver acesso aos vídeos dele, ele faz referência a este artigo sobre o uso de
Volatile
commultithreading
programação.Aqui está o artigo do link acima: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766
Este artigo pode estar um pouco desatualizado, mas dá uma boa ideia para um excelente uso do uso do modificador volátil com no uso de programação multithread para ajudar a manter os eventos assíncronos enquanto o compilador verifica as condições de corrida para nós. Isso pode não responder diretamente à pergunta original do OP sobre a criação de um limite de memória, mas escolho postar isso como uma resposta para os outros, como uma excelente referência para um bom uso de volátil ao trabalhar com aplicativos multithread.
fonte
A palavra-chave
volatile
significa essencialmente que a leitura e a gravação de um objeto devem ser executadas exatamente como escritas pelo programa, e não otimizadas de forma alguma . O código binário deve seguir o código C ou C ++: uma carga onde é lido, uma loja onde há uma gravação.Isso também significa que nenhuma leitura deve resultar em um valor previsível: o compilador não deve assumir nada sobre uma leitura, mesmo imediatamente após uma gravação no mesmo objeto volátil:
volatile int i; i = 1; int j = i; if (j == 1) // not assumed to be true
volatile
pode ser a ferramenta mais importante na caixa de ferramentas "C é uma linguagem assembly de alto nível" .Se declarar um objeto volátil é suficiente para garantir o comportamento do código que lida com mudanças assíncronas depende da plataforma: diferentes CPUs fornecem diferentes níveis de sincronização garantida para leituras e gravações de memória normais. Você provavelmente não deve tentar escrever esse código multithreading de baixo nível, a menos que seja um especialista na área.
Os primitivos atômicos fornecem uma boa visão de alto nível de objetos para multithreading que torna mais fácil raciocinar sobre o código. Quase todos os programadores devem usar primitivos atômicos ou primitivos que fornecem exclusões mútuas, como mutexes, bloqueios de leitura e gravação, semáforos ou outros primitivos de bloqueio.
fonte