Benefício do acesso não volátil a objetos voláteis sendo indefinido?

5

Esta é uma pergunta sobre o ISO C, que contém esta frase:

If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.

Na ISO9899: 1999 (C99) isso aparece em 6.7.3 Type Qualifiers .

Em primeiro lugar, se removêssemos totalmente essa frase do texto, o comportamento ainda estaria indefinido? Ou seja, isso está afirmando uma falta existente de um requisito, ou está explicitamente retirando um requisito que poderia de outro modo ser inferido de outras partes do texto? (Imagine que uma frase foi adicionada dizendo "sempre que dois valores do tipo int são somados, cuja soma aritmética é quarenta e dois, o comportamento é indefinido". Sem essa sentença, a exigência de tal adição produzir um valor pode (e é) inferida).

Em segundo lugar, que comportamentos úteis isso permite que uma implementação forneça? Ou seja, uma implementação pode fazer algo benéfico que interrompe programas que acessam algo volatile com um valor não volatile l e que, portanto, depende da sentença acima? Existem implementações que fazem isso, seja qual for?

Por exemplo, é interessante e digno de nota que os programas em C podem acessar const -qualified objects com valores que não são const , apenas não os armazena. Tornar o comportamento da loja indefinido é altamente benéfico: implementações podem colocar constantes em armazenamento somente leitura e, em alguns casos, podem propagar constantes através de uma imagem de programa no momento da tradução como se fossem constantes de manifesto. No entanto, apenas o acesso não- const é necessário para funcionar. Não exigir que não- const acessos a objetos const funcionem, presumivelmente seria uma permissividade que não traz benefícios. No entanto, no caso de volatile , é estrito: nenhum acesso não- volatile -qualificado é necessário para funcionar. Qual é o benefício?

Pode um objeto que é definido como volatile pelo próprio programa C ser dotado de propriedades úteis que quebram se ocorrer algum acesso a esse objeto através de um valor que não seja volatile ?

(Em "útil", gostaria especificamente de excluir o diagnóstico. Encerrar o programa ao detectar tal acesso, sem uma mensagem ou com uma mensagem como "volátil acessado como não volátil" não conta como útil; serve apenas para impor a falta de exigência.)

(Além disso, não estou interessado em registros mapeados em memória e assim por diante; estritamente objetos definidos pelo programa em armazenamento estático, dinâmico ou automático. O uso de registros de hardware é inerentemente não-portável.)

Tudo o que posso pensar é que um acesso que não seja volatile a um objeto volatile possa recuperar um valor obsoleto, em vez de um valor que foi armazenado pela última vez (corretamente, através do volatile lvalue). Esse é um benefício econômico da seguinte maneira: se uma gravação ocorrer por meio de, digamos, um volatile int lvalue, o compilador não precisará suspeitar que isso tenha qualquer efeito em qualquer int lvalue simples. Isso leva a uma melhor geração de código em funções que trabalham com misturas volatile e objetos que não são volatile do mesmo tipo:

volatile int global;

void foo(int *p)
{
   int x = *p, y;
   global++; 
   y = *p;
}

Aqui, o compilador não precisa considerar que *p pode ser um alias para global , porque esse uso não é necessário para funcionar de acordo com a ISO C. Por isso, pode-se supor que o valor de *p não muda entre as duas referências. Tanto x quanto y recebem o mesmo valor, derivado de um único acesso a *p .

Se o aliasing fosse permitido, o compilador teria que gerar código para recarregar *p no segundo acesso, para que ele pegasse o valor mais recente armazenado em global .

(Se removermos volatile do fragmento de programa acima, então este é de fato o caso; tem que ser pelo menos suspeito, se não confirmado, que *p pode ser um alias para global e assim *p precisa ser reinspecionado ou outro código deve ser gerado, que compara o valor de tempo de execução de p com o endereço de global e usa dois caminhos diferentes.

No entanto, não introduzimos volatile em programas para obter esse tipo de benefício de otimização obscuro; é usado para derrotar a otimização. A palavra-chave restrict pode ser usada para permitir que a implementação pense que lvalues não estão com alias. Parece que o acima não pode ser a razão.

    
por Kaz 24.09.2015 / 22:25
fonte

6 respostas

1

A palavra-chave volatile significa muitas coisas diferentes em muitos compiladores diferentes, mas um de seus usos mais importantes tem sido representar hardware mapeado por memória especial. Assim, declarar o comportamento a esse respeito indefinido permite que os compiladores continuem usando-o para representar esse hardware. A mesma razão que o right-shift, o divide e o módulo de um inteiro assinado negativo não é especificado exatamente: que permite que o conjunto de instruções nativas de cada CPU funcione. Se você quiser um comportamento exatamente definido, existem átomos e div() .

    
por 25.09.2015 / 00:18
fonte
1

Se você estiver interessado apenas em "C portátil", encontrará a palavra-chave volatile para servir pouco ou nenhum uso e, assim, os requisitos parecerão incomuns. Todo o propósito da linguagem é permitir que os compiladores exponham hardware mapeado na memória. Ele obtém uma vida útil mínima se você adicionar uma biblioteca de threads [não portátil] à mistura, mas em sua essência, seu trabalho é fazer comportamentos específicos de implementação. Tanto é assim que a especificação afirma:

An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. ... What constitutes an access to an object that has volatile-qualified type is implementation-defined. (ISO/IEC 9899:TC2 6.7.3.6)

O valor em negar o acesso a um objeto volátil por meios não voláteis se torna aparente nessa linha. A especificação permite que operações "voláteis" sejam implementadas de maneira diferente das operações "não voláteis". Como um exemplo trivial, uma gravação volátil pode procurar um registro mapeado em um mapeamento e, em seguida, gravar no registro, enquanto a gravação não volátil simplesmente grava nessa memória. Esse exemplo pode ser um pouco irreal, mas os desenvolvedores de C fizeram um grande esforço para suportar software de escrita em C em muitas plataformas, e além de adicionar uma linha na especificação (aquela que você mencionou), literalmente não custa nada adicionar esse suporte para o idioma. Ao fazer isso, todos os tipos de situações exóticas de mapeamento de memória são muito mais simples.

Em máquinas x86 modernas, provavelmente não há razão para o compilador tirar proveito disso. Na verdade, eu não ficaria surpreso se os compiladores x86 "comportamento indefinido" neste caso é apenas fazer o que você acha que deveria ter sido feito. No entanto, em plataformas mais anêmicas, a capacidade de não precisar se preocupar com o caso em que um desenvolvedor acessa uma versão volátil por meio de um valor não-volátil pode ser uma diferença substancial na geração de código do compilador.

    
por 24.11.2015 / 10:20
fonte
1

Em um comentário ao Lorehead, Kaz observa corretamente que essa regra só é relevante se o código estiver bem definido. Você não pode fazer código mais Indefinido, afinal, UB é um estado binário.

Então pegue os usos portáteis de voláteis, ou seja, com sinais e longjmp . Em ambos os casos, o compilador deve assegurar que a semântica apropriada esteja sendo respeitada. Em particular, a atualização do estado global visível (volátil) deve ser conforme descrito no código. O estado não volátil pode ser atualizado em qualquer ordem.

Agora vamos supor que uma gravação através de int* possa afetar o estado volátil. Isso significa que qualquer gravação indireta não pode mais ser reordenada. Este é um enorme impacto no desempenho. Pior ainda, todas as leituras através de int* precisam ser feitas e não podem ser armazenadas em cache em um registro.

Assim, esta regra permite otimizações significativas, aumentando maciçamente o número de operações de memória que podem ser reordenadas.

    
por 24.11.2015 / 14:10
fonte
0

Imagine uma máquina hipotética com um local de memória mágica 0xc0103 que, quando gravada, define o brilho de uma luz intermitente no chassi da máquina para o valor armazenado.

Você pode ter um programa como este.

volatile uint32_t * pindicator = (volaitle uint32_t *) 0xc0103;

for (;;)
  {
    uint32_t amount = do_some_work();
    *pindicator = amount;
  }

Isso faria a luz piscar de acordo com a quantidade de trabalho atualmente manipulada pelo sistema. Mesmo que o programa nunca leia o valor de *pindicator , o compilador não tem permissão para otimizar qualquer loja para longe porque ele é qualificado como volatile . O compilador não sabe quais as consequências que um armazenamento para *pindicator pode ter, mas você sabe e pode escrever seu programa de acordo com o fato de o compilador não entrar em seu caminho.

No entanto, se tivermos acesso a ele por meio de um ponteiro que não seja volatile , o compilador estará livre para otimizar qualquer loja que possa provar que não tenha nenhum efeito colateral visível no programa como se regra. Neste exemplo simples, o pior que isso pode fazer é fazer a luz piscar de maneira errática ou, o que é mais provável, ficar para sempre escuro. Mas o padrão C não sabe sobre blinkenlights e 0xc0103 . Ele não pode dizer quais consequências seriam permitidas para acessar a variável através de um ponteiro que não seja volatile . Outra variável pode se referir a um registro de “pulsação” que deve ser incrementado pelo menos uma vez por segundo, e se isso não for feito, o mecanismo de segurança pode disparar um fusível que faz o dispositivo explodir. Assim, a maneira padrão de dizer "qualquer coisa ou nada pode acontecer" é dizer: "o comportamento é indefinido".

    
por 24.11.2015 / 06:59
fonte
0

Muito simplesmente, o acesso não volátil a variáveis voláteis deve ser definido ou indefinido. Então, se você quiser remover a seção que a torna indefinida, você deve torná-la definida. Eu ficaria curioso como você faria isso. Por exemplo, apenas defina o comportamento de

int* p = "address of variable which may or may not be volatile";
*p = 1;

de uma maneira eficiente quando * p não é volátil e definido quando * p é volátil. E lembre-se que os mais usos de ponteiros se enquadram nessa categoria.

    
por 24.11.2015 / 10:11
fonte
-1

O acesso não volátil a uma variável é mais rápido, pois esse acesso pode ser do cache local, mas o que você lê pode ter sido alterado a menos que seja perigoso fazer isso, a menos que você saiba que o valor não muda durante o acesso.

Caso haja 2 modificações não voláteis, como isso é resolvido, o compilador e o hardware dependem.

Como esse tipo de modificação depende do hardware, a maneira mais eficiente de lidar com isso é deixá-lo indefinido para que o compilador possa lidar com ele da melhor maneira possível, permitindo que você se beneficie de acesso não volátil a variáveis voláteis mesmo quando você sabe o que você está fazendo para colher acelerações.

    
por 24.11.2015 / 05:37
fonte