Por que o volátil é necessário em C?


Respostas:


425

Volátil diz ao compilador para não otimizar nada que tenha a ver com a variável volátil.

Há pelo menos três razões comuns para usá-lo, todas envolvendo situações em que o valor da variável pode ser alterado sem ação do código visível: Quando você faz interface com o hardware que altera o próprio valor; quando houver outro thread em execução que também use a variável; ou quando houver um manipulador de sinal que possa alterar o valor da variável.

Digamos que você tenha um pequeno pedaço de hardware mapeado para a RAM em algum lugar e que tenha dois endereços: uma porta de comando e uma porta de dados:

typedef struct
{
  int command;
  int data;
  int isbusy;
} MyHardwareGadget;

Agora você deseja enviar algum comando:

void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
  // wait while the gadget is busy:
  while (gadget->isbusy)
  {
    // do nothing here.
  }
  // set data first:
  gadget->data    = data;
  // writing the command starts the action:
  gadget->command = command;
}

Parece fácil, mas pode falhar porque o compilador é livre para alterar a ordem em que os dados e comandos são gravados. Isso faria nosso pequeno gadget emitir comandos com o valor de dados anterior. Veja também o loop de espera enquanto ocupado. Essa será otimizada. O compilador tentará ser inteligente, ler o valor de isbusy apenas uma vez e depois entrar em um loop infinito. Não é isso que você quer.

A maneira de contornar isso é declarar o dispositivo indicador como volátil. Dessa forma, o compilador é forçado a fazer o que você escreveu. Ele não pode remover as atribuições de memória, não pode armazenar em cache variáveis ​​nos registradores e também não pode alterar a ordem das atribuições:

Esta é a versão correta:

   void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
    {
      // wait while the gadget is busy:
      while (gadget->isbusy)
      {
        // do nothing here.
      }
      // set data first:
      gadget->data    = data;
      // writing the command starts the action:
      gadget->command = command;
    }

46
Pessoalmente, eu prefiro que o tamanho inteiro seja explícito, por exemplo, int8 / int16 / int32 ao conversar com o hardware. Resposta agradável embora;)
tonylo

22
sim, você deve declarar coisas com um tamanho fixo de registro, mas ei - é apenas um exemplo.
Nils Pipenbrinck 29/10/08

69
Volátil também é necessário no código encadeado quando você está jogando com dados que não são protegidos por simultaneidade. E sim, há momentos válidos para fazer isso. Você pode, por exemplo, escrever uma fila de mensagens circular segura de thread sem precisar de proteção explícita à concorrência, mas precisará de voláteis.
Gordon Wrigley

14
Leia mais a especificação C. O volátil apenas definiu o comportamento na E / S do dispositivo mapeado na memória ou na memória tocada por uma função de interrupção assíncrona. Ele não diz nada sobre o encadeamento, e um compilador que otimiza o acesso ausente à memória tocada por vários threads é compatível.
ephemient

17
@ Tolomea: completamente errado. tristes 17 pessoas não sabem disso. volátil não é uma cerca de memória. está relacionado apenas a evitar a elisão do código durante a otimização com base na suposição de efeitos colaterais não visíveis .
precisa saber é o seguinte

188

volatileem C, na verdade, surgiu com o objetivo de não armazenar em cache os valores da variável automaticamente. Ele informará ao compilador para não armazenar em cache o valor dessa variável. Portanto, ele gerará código para tirar o valor da volatilevariável fornecida da memória principal toda vez que a encontrar. Esse mecanismo é usado porque a qualquer momento o valor pode ser modificado pelo sistema operacional ou por qualquer interrupção. Portanto, o uso volatilenos ajudará a acessar o valor novamente todas as vezes.


Entrou em existência? O 'volátil' não foi originalmente emprestado do C ++? Bem, eu me lembro ...
syntaxerror

Isso não é tudo volátil sobre - também proibir alguns reordenação se especificado como volátil ..
FaceBro

4
@FaceBro: O objetivo de volatilepossibilitar aos compiladores otimizar o código e, ao mesmo tempo, permitir que os programadores atinjam a semântica que seria alcançada sem essas otimizações. Os autores do Padrão esperavam que implementações de qualidade suportassem qualquer semântica que fosse útil, considerando suas plataformas de destino e campos de aplicação, e não esperavam que os escritores do compilador procurassem oferecer a semântica de menor qualidade que estivesse em conformidade com o Padrão e não fosse 100% estúpida (note que os autores da Norma reconhecer explicitamente a lógica ...
supercat

1
... é possível que uma implementação esteja em conformidade sem ser de boa qualidade o suficiente para ser realmente adequada para qualquer finalidade, mas eles não acharam necessário evitar isso).
Supercat

1
@syntaxerror, como ele pode ser emprestado do C ++ quando o C era mais de uma década mais antigo que o C ++ (tanto nos primeiros lançamentos quanto nos primeiros padrões)?
Phuclv

178

Outro uso para volatileé manipuladores de sinal. Se você tiver um código como este:

int quit = 0;
while (!quit)
{
    /* very small loop which is completely visible to the compiler */
}

É permitido ao compilador notar que o corpo do loop não toca na quitvariável e converte o loop em um while (true)loop. Mesmo se a quitvariável estiver definida no manipulador de sinal para SIGINTe SIGTERM; o compilador não tem como saber disso.

No entanto, se a quitvariável for declarada volatile, o compilador é forçado a carregá-la sempre, porque pode ser modificado em outro lugar. É exatamente isso que você deseja nesta situação.


quando você diz "o compilador é forçado a carregá-lo sempre, é como quando o compilador decide otimizar uma determinada variável e não declaramos a variável como volátil, no tempo de execução que determinada variável é carregada nos registros da CPU que não estão na memória ?
Amit Singh Tomar

1
@AmitSinghTomar Significa o que diz: Sempre que o código verifica o valor, ele é recarregado. Caso contrário, o compilador pode assumir que funções que não fazem referência à variável não podem modificá-la, portanto, assumindo que CesarB pretendia que o loop acima não fosse definido quit, o compilador pode otimizá-lo em um loop constante, assumindo que não há como quitmudar entre iterações. Nota: isso não é necessariamente um bom substituto para a programação segura de thread real.
Underscore_d

se quit é uma variável global, o compilador não deve otimizar o loop while, correto?
Pierre G.

2
@PierreG. Não, o compilador sempre pode assumir que o código é de thread único, a menos que seja dito o contrário. Ou seja, na ausência de volatileou de outros marcadores, ele assumirá que nada fora do loop modifica essa variável depois que entra no loop, mesmo que seja uma variável global.
CesarB #

1
@PierreG. Sim, tente, por exemplo, a compilação extern int global; void fn(void) { while (global != 0) { } }com gcc -O3 -Se olhar para o arquivo de montagem resultante, na minha máquina que faz movl global(%rip), %eax; testl %eax, %eax; je .L1; .L4: jmp .L4, ou seja, um loop infinito se o global não for zero. Em seguida, tente adicionar volatilee veja a diferença.
CesarB

60

volatileinforma ao compilador que sua variável pode ser alterada por outros meios, além do código que está acessando. por exemplo, pode ser um local de memória mapeado de E / S. Se isso não for especificado nesses casos, alguns acessos variáveis ​​podem ser otimizados, por exemplo, seu conteúdo pode ser mantido em um registro e a localização da memória não é lida novamente.


30

Veja este artigo de Andrei Alexandrescu, " volátil - o melhor amigo do programador multithread "

A palavra-chave volátil foi criada para impedir otimizações do compilador que podem tornar o código incorreto na presença de determinados eventos assíncronos. Por exemplo, se você declarar uma variável primitiva como volátil , o compilador não poderá armazená-la em cache em um registro - uma otimização comum que seria desastrosa se essa variável fosse compartilhada entre vários encadeamentos. Portanto, a regra geral é que, se você tiver variáveis ​​do tipo primitivo que devem ser compartilhadas entre vários encadeamentos, declare essas variáveis voláteis. Mas você pode realmente fazer muito mais com essa palavra-chave: você pode usá-la para capturar código que não é seguro para threads e pode fazê-lo em tempo de compilação. Este artigo mostra como isso é feito; a solução envolve um ponteiro inteligente simples que também facilita a serialização de seções críticas do código.

O artigo se aplica a ambos Ce C++.

Veja também o artigo " C ++ e os perigos do bloqueio com dupla verificação ", de Scott Meyers e Andrei Alexandrescu:

Portanto, ao lidar com alguns locais de memória (por exemplo, portas mapeadas ou memória referenciada por ISRs [Interrupt Service Routines]), algumas otimizações devem ser suspensas. existe volátil para especificar tratamento especial para esses locais, especificamente: (1) o conteúdo de uma variável volátil é "instável" (pode mudar por meios desconhecidos pelo compilador); (2) todas as gravações em dados voláteis são "observáveis"; deve ser executado religiosamente e (3) todas as operações em dados voláteis são executadas na sequência em que aparecem no código-fonte. As duas primeiras regras garantem leitura e escrita adequadas. O último permite a implementação de protocolos de E / S que misturam entrada e saída. Isso é informalmente o que as garantias voláteis de C e C ++.


O padrão especifica se uma leitura é considerada 'comportamento observável' se o valor nunca for usado? Minha impressão é que deveria ser, mas quando afirmei que estava em outro lugar, alguém me desafiou por uma citação. Parece-me que em qualquer plataforma em que uma leitura de uma variável volátil possa ter algum efeito, um compilador deve ser necessário para gerar um código que execute todas as leituras indicadas exatamente uma vez; sem esse requisito, seria difícil escrever código que gerasse uma sequência previsível de leituras.
Supercat 13/10

@ supercat: De acordo com o primeiro artigo, "Se você usar o modificador volátil em uma variável, o compilador não armazenará em cache essa variável nos registros - cada acesso atingirá o local de memória real dessa variável". Além disso, na seção §6.7.3.6 da norma c99, diz: "Um objeto que possui um tipo qualificado de volátil pode ser modificado de maneiras desconhecidas para a implementação ou ter outros efeitos colaterais desconhecidos". Implica ainda que variáveis ​​voláteis não podem ser armazenadas em cache nos registros e que todas as leituras e gravações devem ser executadas em ordem em relação aos pontos de sequência, para que sejam de fato observáveis.
Robert S. Barnes

O último artigo afirma, de fato, explicitamente que as leituras são efeitos colaterais. O primeiro indica que as leituras não podem ser executadas fora de sequência, mas não pareciam impedir a possibilidade de elas serem totalmente eliminadas.
Supercat 14/10

"o compilador não tem permissão para armazená-lo em cache em um registro" - A maioria das arquiteturas RISC são máquinas de registro, portanto, qualquer leitura-modificação-gravação precisa armazenar em cache o objeto em um registro. volatilenão garante atomicidade.
muito honesto para este site

1
@ Olaf: Carregar algo em um registro não é a mesma coisa que armazenar em cache. O armazenamento em cache afetaria o número de cargas ou lojas ou seu tempo.
21818

28

Minha explicação simples é:

Em alguns cenários, com base na lógica ou no código, o compilador fará a otimização de variáveis ​​que acha que não são alteradas. A volatilepalavra-chave impede que uma variável seja otimizada.

Por exemplo:

bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
    // execute logic for the scenario where the USB isn't connected 
}

A partir do código acima, o compilador pode pensar que usb_interface_flagestá definido como 0 e que, no loop while, será zero para sempre. Após a otimização, o compilador o tratará while(true)o tempo todo, resultando em um loop infinito.

Para evitar esse tipo de cenário, declaramos o sinalizador como volátil, dizendo ao compilador que esse valor pode ser alterado por uma interface externa ou outro módulo do programa, ou seja, não o otimize. Esse é o caso de uso para voláteis.


19

Um uso marginal para voláteis é o seguinte. Digamos que você queira calcular a derivada numérica de uma função f:

double der_f(double x)
{
    static const double h = 1e-3;
    return (f(x + h) - f(x)) / h;
}

O problema é que x+h-xgeralmente não é igual a hdevido a erros de arredondamento. Pense nisso: ao subtrair números muito próximos, você perde muitos dígitos significativos que podem arruinar o cálculo da derivada (pense em 1.00001 - 1). Uma possível solução alternativa poderia ser

double der_f2(double x)
{
    static const double h = 1e-3;
    double hh = x + h - x;
    return (f(x + hh) - f(x)) / hh;
}

mas, dependendo das opções da plataforma e do compilador, a segunda linha dessa função pode ser eliminada por um compilador de otimização agressiva. Então você escreve

    volatile double hh = x + h;
    hh -= x;

forçar o compilador a ler o local da memória que contém hh, perdendo uma eventual oportunidade de otimização.


Qual é a diferença entre usar hou hhna fórmula derivada? Quando hhé calculada, a última fórmula a usa como a primeira, sem diferença. Talvez devesse ser (f(x+h) - f(x))/hh?
Sergey Zhukov

2
A diferença entre he hhé que hhé truncada para uma potência negativa de dois pela operação x + h - x. Nesse caso, x + hhe xdiferem exatamente por hh. Você também pode ter sua fórmula, que lhe dará o mesmo resultado, uma vez x + he x + hhsão iguais (é o denominador que é importante aqui).
Alexandre C.

3
Não seria uma maneira mais legível de escrever isso x1=x+h; d = (f(x1)-f(x))/(x1-x)? sem usar o volátil.
Sergey Zhukov

Alguma referência de que um compilador pode acabar com essa segunda linha da função?
CoffeeTableEspresso # 25/18

@CoffeeTableEspresso: Não, desculpe. Quanto mais eu sei sobre ponto flutuante, mais acredito que o compilador só pode otimizá-lo se explicitamente informado, com -ffast-mathou equivalente.
Alexandre C.

11

Existem dois usos. Estes são especialmente usados ​​com mais frequência no desenvolvimento incorporado.

  1. O compilador não otimizará as funções que usam variáveis ​​definidas com palavras-chave voláteis

  2. Volátil é usado para acessar os locais exatos da memória na RAM, ROM, etc ... É usado com mais freqüência para controlar dispositivos mapeados na memória, acessar registros da CPU e localizar locais específicos da memória.

Veja exemplos com a lista de montagem. Uso da palavra-chave "volátil" em C no desenvolvimento incorporado


"O compilador não otimizará as funções que usam variáveis ​​definidas com palavras-chave voláteis" - isso está errado.
muito honesto para este site

10

O volátil também é útil quando você deseja forçar o compilador a não otimizar uma sequência de código específica (por exemplo, para escrever um micro-benchmark).


10

Vou mencionar outro cenário em que os voláteis são importantes.

Suponha que você mapeie um arquivo de memória para E / S mais rápida e esse arquivo possa ser alterado nos bastidores (por exemplo, o arquivo não está no seu disco rígido local, mas é servido na rede por outro computador).

Se você acessar os dados do arquivo mapeado na memória por meio de ponteiros para objetos não voláteis (no nível do código-fonte), o código gerado pelo compilador poderá buscar os mesmos dados várias vezes sem que você esteja ciente.

Se esses dados mudarem, o seu programa poderá usar duas ou mais versões diferentes dos dados e entrar em um estado inconsistente. Isso pode levar não apenas ao comportamento logicamente incorreto do programa, mas também a falhas de segurança exploráveis, se ele processar arquivos não confiáveis ​​ou arquivos de locais não confiáveis.

Se você se preocupa com segurança e deve, este é um cenário importante a ser considerado.


7

volátil significa que o armazenamento provavelmente mudará a qualquer momento e será alterado, mas algo fora do controle do programa do usuário. Isso significa que, se você referenciar a variável, o programa deve sempre verificar o endereço físico (isto é, uma entrada mapeada fifo) e não usá-lo em cache.


Nenhum compilador considera volátil o significado de "endereço físico na RAM" ou "ignorar o cache".
curiousguy


5

Na minha opinião, você não deve esperar muito volatile. Para ilustrar, veja o exemplo da resposta altamente votada de Nils Pipenbrinck .

Eu diria que o exemplo dele não é adequado volatile. volatileé usado apenas para: impedir que o compilador faça otimizações úteis e desejáveis . Não é nada sobre o segmento seguro, acesso atômico ou mesmo ordem de memória.

Nesse exemplo:

    void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
    {
      // wait while the gadget is busy:
      while (gadget->isbusy)
      {
        // do nothing here.
      }
      // set data first:
      gadget->data    = data;
      // writing the command starts the action:
      gadget->command = command;
    }

o gadget->data = dataantes gadget->command = commandsomente é garantido no código compilado pelo compilador. No tempo de execução, o processador ainda pode reordenar a atribuição de dados e comandos, em relação à arquitetura do processador. O hardware pode obter os dados incorretos (suponha que o gadget esteja mapeado para a E / S de hardware). A barreira da memória é necessária entre a atribuição de dados e comandos.


2
Eu diria que o volátil é usado para impedir que o compilador faça otimizações que normalmente seriam úteis e desejáveis. Como está escrito, parece que volatileestá prejudicando o desempenho sem motivo. Se é suficiente, isso dependerá de outros aspectos do sistema que o programador possa conhecer mais do que o compilador. Por outro lado, se um processador garantir que uma instrução para gravar em um determinado endereço liberar o cache da CPU, mas um compilador não tiver como liberar variáveis ​​armazenadas em cache de registro que a CPU não conhece, a descarga do cache seria inútil.
Supercat

5

Na linguagem projetada por Dennis Ritchie, todo acesso a qualquer objeto, exceto objetos automáticos cujo endereço não havia sido obtido, se comportaria como se calculasse o endereço do objeto e depois lesse ou gravasse o armazenamento naquele endereço. Isso tornou o idioma muito poderoso, mas limitou severamente as oportunidades de otimização.

Embora possa ter sido possível adicionar um qualificador que convidaria um compilador a assumir que um objeto específico não seria alterado de maneiras estranhas, essa suposição seria apropriada para a grande maioria dos objetos nos programas em C, e teria Foi impraticável adicionar um qualificador a todos os objetos para os quais essa suposição seria apropriada. Por outro lado, alguns programas precisam usar alguns objetos para os quais tal suposição não seria válida. Para resolver esse problema, o Padrão diz que os compiladores podem assumir que objetos que não são declarados volatilenão terão seu valor observado ou alterado de maneiras que estão fora do controle do compilador ou estariam fora do entendimento de um compilador razoável.

Como várias plataformas podem ter maneiras diferentes pelas quais objetos podem ser observados ou modificados fora do controle de um compilador, é apropriado que os compiladores de qualidade para essas plataformas sejam diferentes no tratamento exato da volatilesemântica. Infelizmente, como o Padrão falhou em sugerir que os compiladores de qualidade destinados à programação de baixo nível em uma plataforma devem lidar de volatilemaneira a reconhecer todos e quaisquer efeitos relevantes de uma operação específica de leitura / gravação nessa plataforma, muitos compiladores não conseguem fazer isso. portanto, de maneiras que tornam mais difícil processar coisas como E / S em segundo plano de uma maneira eficiente, mas que não pode ser interrompida pelas "otimizações" do compilador.


5

Em termos simples, ele diz ao compilador para não fazer nenhuma otimização em uma variável específica. Variáveis ​​que são mapeadas para o registro do dispositivo são modificadas indiretamente pelo dispositivo. Nesse caso, volátil deve ser usado.


1
Existe algo novo nesta resposta que não tenha sido mencionado antes?
slfan

3

Um volátil pode ser alterado de fora do código compilado (por exemplo, um programa pode mapear uma variável volátil para um registro mapeado na memória.) O compilador não aplicará certas otimizações ao código que manipula uma variável volátil - por exemplo, ele ganhou ' não o carregue em um registro sem gravá-lo na memória. Isso é importante ao lidar com registros de hardware.


0

Conforme sugerido por muitos aqui, o uso popular da palavra-chave volátil é ignorar a otimização da variável volátil.

A melhor vantagem que vem à mente, e vale a pena mencionar depois de ler sobre o volátil, é - impedir a reversão da variável no caso de a longjmp. Um salto não local.

O que isto significa?

Simplesmente significa que o último valor será retido após o desenrolamento da pilha , para retornar a algum quadro anterior da pilha; normalmente no caso de algum cenário incorreto.

Como estaria fora do escopo desta questão, não vou entrar em detalhes setjmp/longjmpaqui, mas vale a pena ler sobre isso; e como o recurso de volatilidade pode ser usado para reter o último valor.


-2

não permite que o compilador altere automaticamente os valores das variáveis. uma variável volátil é para uso dinâmico.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.