Por que a volatilidade é necessária em C?

Por que a volatile é necessária em C? Para que isso é usado? O que vai fazer?

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

Há apenas um motivo para usá-lo: quando você faz interface com o hardware.

Digamos que você tenha um pequeno 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ê quer mandar 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 está livre para alterar a ordem na qual os dados e comandos são gravados. Isso faria com que nosso pequeno gadget emitisse comandos com o valor de dados anterior. Também dê uma olhada no ciclo de espera enquanto ocupado. Esse será otimizado. 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 apontador como volátil. Desta forma, o compilador é forçado a fazer o que você escreveu. Não é possível remover as atribuições de memory, não pode armazenar em cache variables ​​nos registradores e 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; } 

Outro uso para volatile é manipuladores de sinais. Se você tem código como este:

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

O compilador tem permissão para perceber que o corpo do loop não toca na variável quit e converte o loop em um loop while (true) . Mesmo se a variável quit estiver configurada no manipulador de sinal para SIGINT e SIGTERM ; o compilador não tem como saber isso.

No entanto, se a variável quit for declarada volatile , o compilador é forçado a carregá-la sempre, porque ela pode ser modificada em outro lugar. Isso é exatamente o que você quer nessa situação.

volatile em C, na verdade, surgiu para o propósito de não armazenar em cache os valores da variável automaticamente. Ele dirá à máquina para não armazenar em cache o valor dessa variável. Por isso, o valor da variável volatile dada da memory principal será usado toda vez que ela for encontrada. Esse mecanismo é usado porque a qualquer momento o valor pode ser modificado pelo SO ou por qualquer interrupção. Portanto, usar o volatile nos ajudará a acessar o valor novamente toda vez.

volatile diz ao compilador que sua variável pode ser alterada por outros meios, do que o código que está acessando. por exemplo, pode ser um local de memory mapeado por E / S. Se isto não for especificado em tais casos, alguns accesss variables ​​podem ser otimizados, por exemplo, seu conteúdo pode ser mantido em um registrador, e a localização da memory não é lida novamente.

Veja este artigo de Andrei Alexandrescu, ” volátil – Melhor amigo do programador multithread ”

A palavra-chave volátil foi desenvolvida para impedir otimizações do compilador que podem tornar o código incorreto na presença de determinados events asynchronouss. Por exemplo, se você declarar uma variável primitiva como volátil , o compilador não poderá armazená-la em cache em um registrador – 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 variables ​​do tipo primitivo que devem ser compartilhadas entre vários segmentos, declare essas variables ​​como voláteis . Mas você pode realmente fazer muito mais com esta palavra-chave: você pode usá-la para capturar código que não seja thread-safe, e você pode fazê-lo em tempo de compilation. Este artigo mostra como isso é feito; A solução envolve um simples ponteiro inteligente que também facilita a serialização de seções críticas de código.

O artigo se aplica a C e C++ .

Veja também o artigo ” C ++ e os Perigos do Bloqueio Duplo-verificado ” por Scott Meyers e Andrei Alexandrescu:

Portanto, ao lidar com alguns locais de memory (por exemplo, portas mapeadas de memory ou memory referenciada por ISRs [rotinas de serviço de interrupção]), algumas otimizações devem ser suspensas. volátil existe para especificar tratamento especial para tais locais, especificamente: (1) o conteúdo de uma variável volátil é “instável” (pode mudar por meios desconhecidos para o 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 seqüência em que aparecem no código-fonte. As duas primeiras regras asseguram leitura e escrita adequadas. O último permite a implementação de protocolos de E / S que misturam input e saída. Isto é informalmente o que as garantias voláteis de C e C ++.

Minha explicação simples é:

Em alguns cenários, com base na lógica ou no código, o compilador fará a otimização das variables ​​que ele acha que não mudam. A palavra-chave volatile 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 } 

Do código acima, o compilador pode pensar que usb_interface_flag é definido como 0, e que no loop while será zero para sempre. Após a otimização, o compilador irá tratá-lo como while(true) o tempo todo, resultando em um loop infinito.

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

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

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

O problema é que x+hx geralmente não é igual a h devido a erros de arredondamento. Pense nisso: quando você subtrai 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 da sua plataforma e dos switches do compilador, a segunda linha dessa function 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 memory contendo hh, perdendo uma eventual oportunidade de otimização.

Existem dois usos. Estes são especialmente utilizados com mais freqüência no desenvolvimento embarcado.

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

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

Veja exemplos com listview de assembly. Uso da palavra-chave “volátil” C no desenvolvimento incorporado

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 gravar um micro-benchmark).

Eu mencionarei outro cenário onde os voláteis são importantes.

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

Se você acessar os dados do arquivo mapeado na memory por meio de pointers para objects 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ê tenha conhecimento disso.

Se esses dados forem alterados, 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, caso ele processe arquivos não confiáveis ​​ou arquivos de locais não confiáveis.

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

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 input mapeada fifo), e não usá-lo de forma cache.

O Wiki diz tudo sobre volatile :

  • volátil (programação de computadores)

E o documento do kernel do Linux também faz uma excelente notação sobre o volatile :

  • Por que a class de tipo “volátil” não deve ser usada

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 registrador mapeado na memory.) O compilador não aplicará certas otimizações ao código que manipula uma variável volátil – por exemplo, ele ganhou ‘ t carregá-lo em um registrador sem gravá-lo na memory. Isso é importante quando se lida com registros de hardware.

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

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

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 = data before gadget->command = command só é garantido apenas no código compilado pelo compilador. Em tempo de execução, o processador ainda possivelmente reordena a atribuição de dados e comandos, em relação à arquitetura do processador. O hardware pode obter os dados errados (suponha que o gadget esteja mapeado para E / S de hardware). A barreira de memory é necessária entre a atribuição de dados e comandos.

Na linguagem projetada por Dennis Ritchie, todo access a qualquer object, a não ser objects automáticos cujo endereço não havia sido tomado, se comportaria como se calculasse o endereço do object e depois lesse ou escrevesse o armazenamento naquele endereço. Isso tornou a linguagem muito poderosa, 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 determinado object não seria alterado de maneiras estranhas, tal suposição seria apropriada para a grande maioria dos objects em programas C, e teria foi impraticável adicionar um qualificador a todos os objects para os quais tal suposição seria apropriada. Por outro lado, alguns programas precisam usar alguns objects para os quais tal suposição não seria válida. Para resolver este problema, o Padrão diz que os compiladores podem assumir que os objects que não são declarados volatile não terão seus valores observados ou alterados de maneiras que estão fora do controle do compilador, ou estariam fora do entendimento razoável do compilador.

Como várias plataformas podem ter diferentes maneiras nas quais objects podem ser observados ou modificados fora do controle de um compilador, é apropriado que os compiladores de qualidade para essas plataformas sejam diferentes em seu tratamento exato da semântica volatile . Infelizmente, como o Standard não sugeriu que os compiladores de qualidade destinados à programação de baixo nível em uma plataforma devam lidar com a volatile de uma maneira que reconheça todos os efeitos relevantes de uma operação de leitura / gravação específica nessa plataforma, muitos compiladores ficam aquém do esperado. Fazendo isso de maneiras que dificultam o processamento de coisas como E / S de fundo de uma maneira eficiente, mas que não pode ser quebrada por “otimizações” do compilador.

ele não permite que o compilador altere automaticamente os valores das variables. uma variável volátil é para uso dynamic.