Olhando para este código:
static int global_var = 0;
int update_three(int val)
{
global_var = val;
return 3;
}
int main()
{
int arr[5];
arr[global_var] = update_three(2);
}
Qual entrada de matriz é atualizada? 0 ou 2?
Existe uma parte na especificação de C que indica a precedência da operação nesse caso específico?
c
language-lawyer
order-of-execution
Jiminion
fonte
fonte
clang
que este trecho de código ative um aviso IMHO.Respostas:
Ordem dos operandos esquerdo e direito
Para executar a atribuição
arr[global_var] = update_three(2)
, a implementação C deve avaliar os operandos e, como efeito colateral, atualizar o valor armazenado do operando esquerdo. C 2018 6.5.16 (que é sobre atribuições) o parágrafo 3 nos diz que não há seqüenciamento nos operandos esquerdo e direito:Isso significa que a implementação C é livre para calcular o lvalue
arr[global_var]
primeiro (calculando o lvalue, queremos dizer a que se refere essa expressão), depois avaliarupdate_three(2)
e finalmente atribuir o valor desse último ao primeiro; ou avaliarupdate_three(2)
primeiro, depois calcular o lvalue e depois atribuir o primeiro ao último; ou para avaliar o lvalue eupdate_three(2)
de alguma maneira misturada e, em seguida, atribua o valor certo ao lvalue esquerdo.Em todos os casos, a atribuição do valor ao lvalue deve vir por último, porque 6.5.16 3 também diz:
Violação de sequenciamento
Alguns podem refletir sobre o comportamento indefinido devido ao uso
global_var
e à atualização separadamente, em violação do 6.5 2, que diz:É bastante familiar para muitos praticantes de C que o comportamento de expressões como
x + x++
não é definido pelo padrão C porque ambos usam o valorx
e o modificam separadamente na mesma expressão sem sequenciamento. No entanto, neste caso, temos uma chamada de função, que fornece algumas seqüências.global_var
é usadoarr[global_var]
e atualizado na chamada de funçãoupdate_three(2)
.6.5.2.2 10 nos diz que há um ponto de sequência antes que a função seja chamada:
Dentro da função,
global_var = val;
é uma expressão plena , e assim é o3
dereturn 3;
, por 6,8 4:Depois, há um ponto de sequência entre essas duas expressões, novamente por 6.8 4:
Assim, a implementação C pode avaliar
arr[global_var]
primeiro e depois executar a chamada de função; nesse caso, existe um ponto de sequência entre eles porque existe um antes da chamada de função ou pode avaliarglobal_var = val;
na chamada de função earr[global_var]
, em seguida , nesse caso um ponto de sequência entre eles porque existe um após a expressão completa. Portanto, o comportamento não é especificado - qualquer uma dessas duas coisas pode ser avaliada primeiro - mas não é indefinido.fonte
O resultado aqui não é especificado .
Enquanto a ordem das operações em uma expressão, que determina como as subexpressões são agrupadas, está bem definida, a ordem da avaliação não é especificada. Nesse caso, significa que
global_var
pode ser lido primeiro ou a chamadaupdate_three
pode acontecer primeiro, mas não há como saber qual.Há não um comportamento indefinido aqui porque uma chamada de função apresenta um ponto de ordem, como faz cada declaração na função incluindo a que modifica
global_var
.Para esclarecer, o padrão C define comportamento indefinido na seção 3.4.3 como:
e define o comportamento não especificado na seção 3.4.4 como:
O padrão declara que a ordem de avaliação dos argumentos da função não é especificada, o que neste caso significa que
arr[0]
é definido como 3 ouarr[2]
definido como 3.fonte
Eu tentei e consegui a entrada 0 atualizada.
No entanto, de acordo com esta pergunta: o lado direito de uma expressão sempre será avaliado primeiro
A ordem da avaliação não é especificada e não tem seqüência. Então, acho que um código como esse deve ser evitado.
fonte
Como não faz sentido emitir código para uma atribuição antes que você tenha um valor para atribuir, a maioria dos compiladores C emitirá primeiro o código que chama a função e salva o resultado em algum lugar (registrador, pilha etc.), depois emitirá o código que grava esse valor em seu destino final e, portanto, eles lerão a variável global depois que ela for alterada. Vamos chamar isso de "ordem natural", não definida por nenhum padrão, mas pela pura lógica.
No entanto, no processo de otimização, os compiladores tentarão eliminar a etapa intermediária de armazenar temporariamente o valor em algum lugar e tentar gravar o resultado da função o mais diretamente possível ao destino final e, nesse caso, eles geralmente terão que ler o índice primeiro , por exemplo, para um registrador, para poder mover diretamente o resultado da função para o array. Isso pode fazer com que a variável global seja lida antes de ser alterada.
Portanto, esse é basicamente um comportamento indefinido com a propriedade muito ruim que é bem provável que o resultado seja diferente, dependendo se a otimização é executada e quão agressiva é essa otimização. É sua tarefa como desenvolvedor resolver esse problema através da codificação:
ou codificação:
Como uma boa regra geral: a menos que as variáveis globais sejam
const
(ou não sejam, mas você saiba que nenhum código as alterará como efeito colateral), você nunca deve usá-las diretamente no código, como em um ambiente com vários threads, mesmo isso pode ser indefinido:Como o compilador pode lê-lo duas vezes e outro encadeamento pode alterar o valor entre as duas leituras. No entanto, novamente, a otimização definitivamente faria com que o código o lesse apenas uma vez; portanto, você pode ter resultados diferentes que agora também dependem do tempo de outro encadeamento. Portanto, você terá muito menos dor de cabeça se armazenar variáveis globais em uma variável de pilha temporária antes do uso. Lembre-se de que, se o compilador achar que isso é seguro, provavelmente otimizará isso e usará a variável global diretamente. Portanto, no final, pode não fazer diferença no desempenho ou no uso da memória.
(Apenas no caso de alguém perguntar por que alguém faria isso em
x + 2 * x
vez de3 * x
- em algumas CPUs, a adição é ultra-rápida e a multiplicação é por uma potência dois, pois o compilador as transforma em turnos de bits (2 * x == x << 1
), mas a multiplicação com números arbitrários pode ser muito lenta , portanto, em vez de multiplicar por 3, você obtém um código muito mais rápido deslocando x por 1 e adicionando x ao resultado - e até esse truque é executado pelos compiladores modernos se você multiplicar por 3 e ativar a otimização agressiva, a menos que seja um destino moderno CPU em que a multiplicação é igualmente rápida como adição desde então, o truque atrasaria o cálculo.)fonte
3 * x
em duas leituras de x. Ele pode ler x uma vez e, em seguida, executar o método x + 2 * x no registro em que lê xlanguage-lawyer
, onde o idioma em questão tem seu próprio "significado muito especial" para indefinido , você só causará confusão se não usar a definição da linguagem.Edição global: desculpe pessoal, eu fiquei demitida e escrevi muitas bobagens. Apenas um velhote velho reclamando.
Eu queria acreditar que o C havia sido poupado, mas, desde o C11, ele foi comparado com o C ++. Aparentemente, saber o que o compilador fará com efeitos colaterais em expressões requer agora resolver um pequeno enigma matemático envolvendo uma ordenação parcial de seqüências de código com base em um "está localizado antes do ponto de sincronização de".
Por acaso, eu projetei e implementei alguns sistemas embarcados críticos em tempo real nos dias de K&R (incluindo o controlador de um carro elétrico que poderia fazer com que as pessoas colidissem com a parede mais próxima se o motor não fosse controlado, um industrial de 10 toneladas um robô que poderia esmagar as pessoas em uma pasta, se não fosse comandado adequadamente, e uma camada de sistema que, apesar de inofensiva, teria algumas dezenas de processadores sugando seu barramento de dados com menos de 1% de sobrecarga do sistema).
Talvez eu seja muito senil ou estúpido para obter a diferença entre indefinido e não especificado, mas acho que ainda tenho uma boa idéia do que significa execução simultânea e acesso a dados. Na minha opinião indiscutivelmente informada, essa obsessão do C ++ e agora do C com seus idiomas de estimação assumindo problemas de sincronização é um sonho caro. Você sabe o que é execução simultânea e não precisa de nenhum desses dispositivos, ou não, e faria um favor ao mundo em geral, sem tentar mexer com ele.
Todo esse caminhão de abstrações de barreira de memória de dar água nos olhos é simplesmente devido a um conjunto temporário de limitações dos sistemas de cache com várias CPUs, que podem ser encapsulados com segurança em objetos comuns de sincronização do SO, como, por exemplo, os mutexes e variáveis de condição C ++ ofertas.
O custo desse encapsulamento é apenas uma queda minúscula no desempenho, em comparação com o que um uso de instruções específicas da CPU de baixa granularidade poderia alcançar em alguns casos.
A
volatile
palavra-chave (ou um#pragma dont-mess-with-that-variable
por tudo que eu, como programador de sistemas, cuidado) teria sido suficiente para dizer ao compilador para parar de reordenar os acessos à memória. O código ideal pode ser facilmente produzido com diretivas diretas asm para espalhar códigos de driver e SO de baixo nível com instruções específicas da CPU ad hoc. Sem um conhecimento profundo de como o hardware subjacente (sistema de cache ou interface de barramento) funciona, você provavelmente escreverá um código inútil, ineficiente ou com defeito.Um ajuste minucioso da
volatile
palavra - chave e de Bob teria sido todo mundo, menos o tio dos programadores de nível mais baixo. Em vez disso, a turma habitual de loucos por matemática em C ++ teve um dia de campo projetando mais uma abstração incompreensível, cedendo à tendência típica de projetar soluções procurando problemas inexistentes e confundindo a definição de uma linguagem de programação com as especificações de um compilador.Somente desta vez a mudança necessária para desfigurar um aspecto fundamental de C também, uma vez que essas "barreiras" precisavam ser geradas mesmo em código C de baixo nível para funcionar corretamente. Que, entre outras coisas, causou estragos na definição de expressões, sem nenhuma explicação ou justificativa.
Como conclusão, o fato de um compilador produzir um código de máquina consistente a partir dessa parte absurda de C é apenas uma conseqüência distante da maneira como os funcionários do C ++ lidaram com possíveis inconsistências nos sistemas de cache do final dos anos 2000.
Isso causou uma confusão terrível em um aspecto fundamental de C (definição de expressão), de modo que a grande maioria dos programadores de C - que não se importam com os sistemas de cache, e com razão - agora são obrigados a confiar nos gurus para explicar a diferença entre
a = b() + c()
ea = b + c
.Tentar adivinhar o que será dessa matriz infeliz é uma perda líquida de tempo e esforços de qualquer maneira. Independentemente do que o compilador faça disso, esse código está patologicamente errado. A única coisa responsável a fazer com isso é enviá-lo para a lixeira.
Conceitualmente, os efeitos colaterais sempre podem ser removidos das expressões, com o esforço trivial de permitir explicitamente que a modificação ocorra antes ou depois da avaliação, em uma declaração separada.
Esse tipo de código de merda pode ter sido justificado nos anos 80, quando você não esperava que um compilador otimizasse nada. Mas agora que os compiladores há muito se tornaram mais inteligentes do que a maioria dos programadores, tudo o que resta é um pedaço de código de merda.
Também não entendo a importância desse debate indefinido / não especificado. Você pode confiar no compilador para gerar código com um comportamento consistente ou não pode. Se você chama isso de indefinido ou não especificado, parece um ponto discutível.
Na minha opinião indiscutivelmente informada, C já é perigoso o suficiente em seu estado de K&R. Uma evolução útil seria adicionar medidas de segurança de senso comum. Por exemplo, ao usar essa ferramenta avançada de análise de código, as especificações forçam o compilador a implementar para, pelo menos, gerar avisos sobre o código bonkers, em vez de gerar silenciosamente um código potencialmente não confiável ao extremo.
Mas, em vez disso, os caras decidiram, por exemplo, definir uma ordem de avaliação fixa no C ++ 17. Agora, todo imbecil de software é incitado ativamente a colocar efeitos colaterais em seu código de propósito, aproveitando a certeza de que os novos compiladores lidarão ansiosamente com a ofuscação de uma maneira determinística.
K&R foi uma das verdadeiras maravilhas do mundo da computação. Por vinte dólares, você obtém uma especificação abrangente da linguagem (vi indivíduos solteiros escreverem compiladores completos usando apenas este livro), um excelente manual de referência (o índice geralmente indica algumas páginas da resposta à sua pergunta). questão) e um livro didático que ensina a usar o idioma de maneira sensata. Complete com argumentos, exemplos e sábias palavras de aviso sobre as inúmeras maneiras pelas quais você pode abusar do idioma para fazer coisas muito, muito estúpidas.
Destruir essa herança por tão pouco ganho parece um desperdício cruel para mim. Mas, novamente, eu poderia muito bem deixar de entender completamente o ponto. Talvez uma alma amável possa me apontar na direção de um exemplo de novo código C que aproveite significativamente esses efeitos colaterais.
fonte
0,expr,0
.