Por que é volatile
necessário em C? Para que isso é usado? O que isso fará?
Por que é volatile
necessário em C? Para que isso é usado? O que isso fará?
Respostas:
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;
}
volatile
em 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 volatile
variá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 volatile
nos ajudará a acessar o valor novamente todas as vezes.
volatile
possibilitar 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 ...
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 quit
variável e converte o loop em um while (true)
loop. Mesmo se a quit
variável estiver definida no manipulador de sinal para SIGINT
e SIGTERM
; o compilador não tem como saber disso.
No entanto, se a quit
variá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.
quit
, o compilador pode otimizá-lo em um loop constante, assumindo que não há como quit
mudar entre iterações. Nota: isso não é necessariamente um bom substituto para a programação segura de thread real.
volatile
ou 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.
extern int global; void fn(void) { while (global != 0) { } }
com gcc -O3 -S
e 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 volatile
e veja a diferença.
volatile
informa 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.
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 C
e 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 ++.
volatile
não garante atomicidade.
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 volatile
palavra-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_flag
está 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.
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-x
geralmente não é igual a h
devido 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.
h
ou hh
na 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
?
h
e hh
é que hh
é truncada para uma potência negativa de dois pela operação x + h - x
. Nesse caso, x + hh
e x
diferem exatamente por hh
. Você também pode ter sua fórmula, que lhe dará o mesmo resultado, uma vez x + h
e x + hh
são iguais (é o denominador que é importante aqui).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? sem usar o volátil.
-ffast-math
ou equivalente.
Existem dois usos. Estes são especialmente usados com mais frequência no desenvolvimento incorporado.
O compilador não otimizará as funções que usam variáveis definidas com palavras-chave voláteis
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 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).
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.
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.
O Wiki diz tudo sobre volatile
:
E o documento do kernel Linux também faz uma excelente notação sobre volatile
:
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 = data
antes gadget->command = command
somente é 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.
volatile
está 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.
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 volatile
nã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 volatile
semâ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 volatile
maneira 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.
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.
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.
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/longjmp
aqui, mas vale a pena ler sobre isso; e como o recurso de volatilidade pode ser usado para reter o último valor.