Depois de olhar para um monte de outras questões e suas respostas , fico com a impressão de que não há um acordo generalizado sobre o que a palavra-chave "volátil" em C significa exatamente.
Mesmo o próprio padrão não parece suficientemente claro para que todos concordem com o que isso significa .
Entre outros problemas:
- Parece fornecer garantias diferentes, dependendo do seu hardware e do seu compilador.
- Isso afeta as otimizações do compilador, mas não as otimizações de hardware; portanto, em um processador avançado que faz suas próprias otimizações em tempo de execução, não é claro se o compilador pode impedir a otimização que você deseja impedir. (Alguns compiladores geram instruções para impedir algumas otimizações de hardware em alguns sistemas, mas isso não parece ser padronizado de forma alguma.)
Para resumir o problema, parece (depois de ler muito) que "volátil" garante algo como: O valor será lido / gravado não apenas de / para um registro, mas pelo menos no cache L1 do núcleo, na mesma ordem em que as leituras / gravações aparecem no código. Mas isso parece inútil, já que a leitura / gravação de / para um registro já é suficiente no mesmo encadeamento, enquanto a coordenação com o cache L1 não garante mais nada em relação à coordenação com outros encadeamentos. Não consigo imaginar quando seria importante sincronizar apenas com o cache L1.
USO 1
O único uso amplamente aceito e volátil de sistemas voláteis parece ser para sistemas antigos ou embutidos em que determinados locais de memória são mapeados por hardware para funções de E / S, como um pouco na memória que controla (diretamente, no hardware) uma luz , ou um pouco na memória que informa se uma tecla do teclado está pressionada ou não (porque está conectada pelo hardware diretamente à tecla).
Parece que o "uso 1" não ocorre no código portátil cujos destinos incluem sistemas com vários núcleos.
USE 2
Não é muito diferente de "use 1" a memória que pode ser lida ou gravada a qualquer momento por um manipulador de interrupções (que pode controlar uma luz ou armazenar informações de uma tecla). Mas já para isso, temos o problema de que, dependendo do sistema, o manipulador de interrupções pode ser executado em um núcleo diferente com seu próprio cache de memória e "volátil" não garante a coerência do cache em todos os sistemas.
Portanto, o "uso 2" parece estar além do que o "volátil" pode oferecer.
USO 3
O único outro uso indiscutível que vejo é impedir a otimização incorreta dos acessos por meio de diferentes variáveis apontando para a mesma memória que o compilador não percebe que é a mesma memória. Mas isso provavelmente é indiscutível porque as pessoas não estão falando sobre isso - eu só vi uma menção a isso. E eu pensei que o padrão C já reconheceu que ponteiros "diferentes" (como argumentos diferentes para uma função) podem apontar para o mesmo item ou itens próximos, e já especifiquei que o compilador deve produzir código que funcione mesmo nesses casos. No entanto, não consegui encontrar rapidamente este tópico no padrão mais recente (500 páginas!).
Então "use 3" talvez não exista ?
Daí a minha pergunta:
"Volátil" garante alguma coisa no código C portátil para sistemas com vários núcleos?
EDIT - atualização
Depois de procurar o padrão mais recente , parece que a resposta é pelo menos um sim muito limitado:
1. O padrão especifica repetidamente o tratamento especial para o tipo específico "sig_atomic_t volátil". No entanto, o padrão também diz que o uso da função de sinal em um programa multithread resulta em comportamento indefinido. Portanto, esse caso de uso parece limitado à comunicação entre um programa de thread único e seu manipulador de sinais.
2. O padrão também especifica um significado claro para "volátil" em relação a setjmp / longjmp. (O exemplo de código onde é importante é fornecido em outras perguntas e respostas .)
Portanto, a pergunta mais precisa se torna:
"volátil" garante alguma coisa no código C portátil para sistemas com vários núcleos, além de (1) permitir que um programa de thread único receba informações de seu manipulador de sinal ou (2) permitir setjmp código para ver variáveis modificadas entre setjmp e longjmp?
Ainda é uma pergunta de sim / não.
Se "yes", seria ótimo se você pudesse mostrar um exemplo de código portátil sem erros que se tornaria buggy se "volatile" for omitido. Se "não", suponho que um compilador esteja livre para ignorar "volátil" fora desses dois casos muito específicos, para destinos com vários núcleos.
volatile
informar o programa que ele pode ser alterado de forma assíncrona.volatile
especificamente, o que acredito ser necessário.Respostas:
Não, absolutamente não . E isso torna volátil quase inútil para o propósito do código seguro MT.
Se isso acontecesse, então volátil seria muito bom para variáveis compartilhadas por vários threads, pois a ordem dos eventos no cache L1 é tudo o que você precisa fazer na CPU típica (que é multi-core ou multi-CPU na placa-mãe) capaz de cooperar de uma maneira que possibilite uma implementação normal de multithreading C / C ++ ou Java com custos típicos esperados (isto é, não um custo enorme na maioria das operações mutex atômicas ou sem conteúdo).
Porém, o volátil não fornece nenhuma ordem garantida (ou "visibilidade da memória") no cache, na teoria ou na prática.
(Nota: o seguinte é baseado na interpretação sólida dos documentos padrão, na intenção do padrão, na prática histórica e em um profundo entendimento das expectativas dos escritores do compilador. Essa abordagem baseia-se na história, nas práticas reais, nas expectativas e no entendimento de pessoas reais em o mundo real, que é muito mais forte e mais confiável do que analisar as palavras de um documento que não é conhecido por escrever especificações estelares e que foi revisado várias vezes.)
Na prática, o volátil garante a capacidade de rastrear, que é a capacidade de usar informações de depuração para o programa em execução, em qualquer nível de otimização , e o fato de que as informações de depuração fazem sentido para esses objetos voláteis:
ptrace
(um mecanismo semelhante ao ptrace) para definir pontos de interrupção significativos nos pontos de sequência após operações envolvendo objetos voláteis: você pode realmente quebrar exatamente nesses pontos (observe que isso funciona apenas se você estiver disposto a definir muitos pontos de interrupção como qualquer outro) A instrução C / C ++ pode ser compilada em muitos pontos diferentes de início e de final da montagem, como em um loop massivamente desenrolado);A garantia volátil na prática é um pouco mais do que a estrita interpretação do ptrace: também garante que variáveis automáticas voláteis tenham um endereço na pilha, pois não são alocadas a um registro, uma alocação de registro que tornaria as manipulações do ptrace mais delicadas (o compilador pode informações de depuração de saída para explicar como as variáveis são alocadas aos registradores, mas ler e alterar o estado do registrador é um pouco mais envolvido do que acessar endereços de memória).
Observe que a capacidade total de depuração do programa, que considera todas as variáveis voláteis, pelo menos nos pontos de sequência, é fornecida pelo modo "otimização zero" do compilador, um modo que ainda realiza otimizações triviais como simplificações aritméticas (geralmente não há garantia de otimização em todos os modos). Mas volátil é mais forte que a não otimização:
x-x
pode ser simplificado para um número inteiro não volátil,x
mas não para um objeto volátil.Portanto, meios voláteis garantidos para serem compilados como estão , como a tradução da origem para o binário / assembly pelo compilador de uma chamada do sistema não são uma reinterpretação, alterada ou otimizada de qualquer forma por um compilador. Observe que as chamadas da biblioteca podem ou não ser chamadas do sistema. Muitas funções oficiais do sistema são, na verdade, funções de biblioteca que oferecem uma fina camada de interposição e geralmente adiam para o kernel no final. (Em particular,
getpid
não é necessário acessar o kernel e pode ler uma localização de memória fornecida pelo sistema operacional que contém as informações.)Interações voláteis são interações com o mundo exterior da máquina real , que devem seguir a "máquina abstrata". Eles não são interações internas das partes do programa com outras partes do programa. O compilador só pode raciocinar sobre o que sabe, ou seja, as partes internas do programa.
A geração de código para um acesso volátil deve seguir a interação mais natural com esse local de memória: não deve surpreender. Isso significa que é esperado que alguns acessos voláteis sejam atômicos : se a maneira natural de ler ou gravar a representação de um
long
na arquitetura é atômica, é esperado que uma leitura ou gravação de umvolatile long
seja atômica, pois o compilador não deve gerar código tolo e ineficiente para acessar objetos voláteis byte a byte, por exemplo .Você deve poder determinar isso conhecendo a arquitetura. Você não precisa saber nada sobre o compilador, pois volátil significa que o compilador deve ser transparente .
Porém, o volátil não força mais do que forçar a emissão do assembly esperado para os menos otimizados para casos específicos para executar uma operação de memória: semântica volátil significa semântica geral de caso.
O caso geral é o que o compilador faz quando não possui nenhuma informação sobre uma construção: f.ex. chamar uma função virtual em um lvalue por meio de despacho dinâmico é um caso geral, fazer uma chamada direta ao overrider após determinar em tempo de compilação o tipo de objeto designado pela expressão é um caso específico. O compilador sempre tem um tratamento geral de caso de todas as construções e segue a ABI.
O volátil não faz nada de especial para sincronizar threads ou fornecer "visibilidade da memória": o volátil fornece apenas garantias no nível abstrato visto de dentro de um thread em execução ou parado, que é o interior de um núcleo de CPU :
Somente o segundo ponto significa que o volátil não é útil na maioria dos problemas de comunicação entre threads; o primeiro ponto é essencialmente irrelevante em qualquer problema de programação que não envolva comunicação com componentes de hardware fora da (s) CPU (s), mas ainda no barramento de memória.
A propriedade volátil de fornecer comportamento garantido do ponto de vista do núcleo que está executando o encadeamento significa que os sinais assíncronos entregues a esse encadeamento, que são executados do ponto de vista da ordem de execução desse encadeamento, ver operações na ordem do código-fonte .
A menos que você planeje enviar sinais para seus encadeamentos (uma abordagem extremamente útil para a consolidação de informações sobre encadeamentos atualmente em execução, sem um ponto de parada previamente acordado), o volátil não é para você.
fonte
Não sou especialista, mas o cppreference.com tem o que parece ser uma informação
volatile
muito boa sobre . Aqui está a essência:Também fornece alguns usos:
E, claro, menciona que
volatile
não é útil para sincronização de threads:fonte
longjmp
no código C ++.Antes de tudo, historicamente houve vários problemas com relação a diferentes interpretações do significado de
volatile
acesso e similares. Veja este estudo: Voláteis são mal compilados e o que fazer sobre isso .Além dos vários problemas mencionados nesse estudo, o comportamento de
volatile
é portátil, exceto por um aspecto deles: quando eles agem como barreiras de memória . Uma barreira de memória é um mecanismo que existe para impedir a execução simultânea e não consecutiva do seu código. Usarvolatile
como barreira de memória certamente não é portátil.Se a linguagem C garante ou não o comportamento da memória
volatile
é aparentemente discutível, embora pessoalmente eu ache que a linguagem é clara. Primeiro, temos a definição formal de efeitos colaterais, C17 5.1.2.3:A norma define o termo sequenciamento, como uma maneira de determinar a ordem da avaliação (execução). A definição é formal e complicada:
O TL; DR acima é basicamente que, no caso de termos uma expressão
A
que contém efeitos colaterais, ela deve ser executada antes de outra expressãoB
, casoB
seja sequenciada posteriormenteA
.Otimizações do código C são possíveis através desta parte:
Isso significa que o programa pode avaliar (executar) expressões na ordem que o padrão exige em outros lugares (ordem de avaliação, etc.). Mas ele não precisa avaliar (executar) um valor se puder deduzir que não é usado. Por exemplo, a operação
0 * x
não precisa avaliarx
e simplesmente substituir a expressão por0
.A menos que acessar uma variável seja um efeito colateral. Isso significa que, caso
x
sejavolatile
, ele deve avaliar (executar)0 * x
mesmo que o resultado sempre seja 0. A otimização não é permitida.Além disso, o padrão fala de comportamento observável:
Dado todo o exposto, uma implementação em conformidade (compilador + sistema subjacente) pode não executar o acesso a
volatile
objetos em uma ordem não sequencial, caso a semântica da fonte C escrita diga o contrário.Isso significa que neste exemplo
Ambas as expressões de atribuição deve ser avaliado e
z = x;
deve ser avaliada antesz = y;
. Uma implementação de multiprocessador que terceiriza essas duas operações para dois núcleos de sequências diferentes não está em conformidade!O dilema é que os compiladores não podem fazer muito sobre coisas como cache de pré-busca e pipelining de instruções, etc., principalmente quando executados em cima de um sistema operacional. E assim os compiladores entregam esse problema aos programadores, dizendo que as barreiras de memória agora são de responsabilidade do programador. Enquanto o padrão C afirma claramente que o problema precisa ser resolvido pelo compilador.
O compilador não necessariamente se preocupa em resolver o problema e,
volatile
por isso, agir como uma barreira de memória não é portátil. Tornou-se um problema de qualidade de implementação.fonte
z
seriam realmente executadas? (comoz = x; z = y;
) O valor será apagado na próxima instrução.z
realmente atribuído duas vezes? Como você sabe que "as leituras são executadas"?