O código
int a = ((1 + 2) + 3); // Easy to read
correr mais devagar que
int a = 1 + 2 + 3; // (Barely) Not quite so easy to read
ou os compiladores modernos são inteligentes o suficiente para remover / otimizar parênteses "inúteis".
Pode parecer uma preocupação muito pequena de otimização, mas escolher C ++ em C # / Java / ... tem tudo a ver com otimizações (IMHO).
c++
optimization
compilation
Sarja
fonte
fonte
Respostas:
O compilador, na verdade, nunca insere ou remove parênteses; apenas cria uma árvore de análise (na qual não há parênteses) correspondente à sua expressão e, ao fazê-lo, deve respeitar os parênteses que você escreveu. Se você colocar um parêntese completo em sua expressão, também ficará imediatamente claro para o leitor humano o que é essa árvore de análise; se você for ao extremo de colocar parênteses flagrantemente redundantes como em
int a = (((0)));
, estará causando algum estresse inútil nos neurônios do leitor, além de desperdiçar alguns ciclos no analisador, sem alterar a árvore de análise resultante (e, portanto, o código gerado ) um pouquinho.Se você não escrever parênteses, o analisador ainda deve criar sua árvore de análise, e as regras para precedência e associatividade do operador informam exatamente qual árvore de análise deve ser construída. Você pode considerar essas regras dizendo ao compilador quais parênteses (implícitos) ele deve inserir em seu código, embora o analisador nunca lide com parênteses neste caso: ele foi construído para produzir a mesma árvore de análise como se parênteses estavam presentes em certos lugares. Se você colocar parênteses exatamente nesses lugares, como em
int a = (1+2)+3;
(associatividade de+
é à esquerda), o analisador chegará ao mesmo resultado por uma rota ligeiramente diferente. Se você colocar parênteses diferentes, como emint a = 1+(2+3);
você está forçando uma árvore de análise diferente, o que possivelmente fará com que código diferente seja gerado (embora talvez não, pois o compilador pode aplicar transformações após a construção da árvore de análise, desde que o efeito da execução do código resultante nunca seja diferente para isto). Supondo que haja uma diferença no código de resolução, nada em geral pode ser dito sobre qual é mais eficiente; o ponto mais importante é, é claro, que na maioria das vezes as árvores de análise não dão expressões matematicamente equivalentes; portanto, comparar a velocidade de execução não é o ponto: basta escrever a expressão que dá o resultado apropriado.Portanto, o resultado é: use parênteses conforme necessário para correção e conforme desejado para legibilidade; se redundantes, eles não têm nenhum efeito na velocidade de execução (e um efeito insignificante no tempo de compilação).
E nada disso tem a ver com a otimização , que ocorre após a construção da árvore de análise, portanto, não é possível saber como a árvore de análise foi construída. Isso se aplica sem alterações dos compiladores mais antigos e estúpidos aos mais inteligentes e modernos. Somente em um idioma interpretado (onde "tempo de compilação" e "tempo de execução" são coincidentes) é possível que haja uma penalidade por parênteses redundantes, mas mesmo assim acho que a maioria desses idiomas é organizada para que pelo menos a fase de análise seja realizada apenas uma vez para cada instrução (armazenando alguma forma pré-analisada para execução).
fonte
a = b + c * d;
,a = b + (c * d);
seriam parênteses redundantes [inofensivos]. Se eles ajudarem a tornar o código mais legível, tudo bem.a = (b + c) * d;
seriam parênteses não redundantes - eles realmente alteram a árvore de análise resultante e fornecem um resultado diferente. Perfeitamente legal de fazer (de fato, necessário), mas não é o mesmo que o agrupamento padrão implícito.Os parênteses existem apenas para seu benefício - não os compiladores. O compilador criará o código de máquina correto para representar sua declaração.
Para sua informação, o compilador é inteligente o suficiente para otimizá-lo completamente, se puder. Nos seus exemplos, isso seria transformado em
int a = 6;
tempo de compilação.fonte
int a = 8;// = 2*3 + 5
int five = 7; //HR made us change this to six...
A resposta para a pergunta que você realmente fez é não, mas a resposta para a pergunta que você queria fazer é sim. Adicionar parênteses não diminui a velocidade do código.
Você fez uma pergunta sobre otimização, mas os parênteses não têm nada a ver com otimização. O compilador aplica uma variedade de técnicas de otimização com a intenção de melhorar o tamanho ou a velocidade do código gerado (às vezes ambos). Por exemplo, ela pode pegar a expressão A ^ 2 (A ao quadrado) e substituí-la por A x A (A multiplicado por si só) se isso for mais rápido. A resposta aqui é não, o compilador não faz nada diferente em sua fase de otimização, dependendo da presença de parênteses.
Acho que você quis perguntar se o compilador ainda gera o mesmo código se você adicionar parênteses desnecessários a uma expressão, em lugares que você acha que podem melhorar a legibilidade. Em outras palavras, se você adicionar parênteses, o compilador é inteligente o suficiente para removê-los novamente, em vez de gerar, de alguma forma, um código mais pobre. A resposta é sim, sempre.
Deixe-me dizer isso com cuidado. Se você adicionar parênteses a uma expressão estritamente desnecessária (sem nenhum efeito no significado ou na ordem de avaliação de uma expressão), o compilador as descartará silenciosamente e gerará o mesmo código.
No entanto, existem certas expressões em que parênteses aparentemente desnecessários realmente alteram a ordem de avaliação de uma expressão e, nesse caso, o compilador gerará código para efetivar o que você realmente escreveu, o que pode ser diferente do que você pretendia. Aqui está um exemplo. Não faça isso!
Portanto, adicione parênteses, se quiser, mas verifique se eles são realmente desnecessários!
Eu nunca faço. Existe um risco de erro sem benefício real.
Nota de rodapé: o comportamento não assinado acontece quando uma expressão inteira assinada é avaliada como um valor que está fora do intervalo que pode ser expresso, neste caso, -32767 a +32767. Este é um tópico complexo, fora do escopo desta resposta.
fonte
a
puder realmente ser não assinado, o cálculo do seu cálculo-a + b
poderia facilmente estourar também, sea
fosse negativo eb
positivo.(b+c)
na última linha promoverá seus argumentos paraint
, portanto, a menos que o compilador definaint
16 bits (seja por ser antigo ou direcionado a um pequeno microcontrolador), a última linha seria perfeitamente legítima.int
promovido, aint
menos que esse tipo seja incapaz de representar todos os seus valores, caso em que é promovidounsigned int
. Os compiladores podem omitir as promoções se todos os comportamentos definidos forem iguais, como se as promoções fossem incluídas . Em uma máquina em que os tipos de 16 bits se comportam como um anel algébrico abstrato, (a + b) + ce + (b + c) serão equivalentes. Seint
fosse um tipo de 16 bits que preso em excesso, no entanto, então não haveria casos em que uma das expressões ...Os colchetes existem apenas para você manipular a ordem de precedência do operador. Uma vez compilados, os colchetes não existem mais porque o tempo de execução não precisa deles. O processo de compilação remove todos os colchetes, espaços e outros tipos de açúcar sintático que você e eu precisamos e transforma todos os operadores em algo muito mais simples para a execução do computador.
Então, onde você e eu podemos ver ...
... um compilador pode emitir algo mais parecido com isto:
O programa é executado iniciando no início e executando cada instrução sucessivamente.
A precedência do operador agora é "primeiro a chegar, primeiro a ser servido".
Tudo é fortemente digitado, porque o compilador trabalhou tudo isso enquanto rasgava a sintaxe original.
OK, não é nada parecido com o que você e eu lidamos, mas não estamos executando!
fonte
Depende se é ponto flutuante ou não:
No ponto flutuante, a adição aritmética não é associativa, portanto, o otimizador não pode reordenar as operações (a menos que você inclua a opção do compilador fastmath).
Em operações inteiras, eles podem ser reordenados.
No seu exemplo, ambos serão executados exatamente no mesmo horário, pois serão compilados exatamente no mesmo código (a adição é avaliada da esquerda para a direita).
no entanto, mesmo java e C # poderão otimizá-lo, eles o farão em tempo de execução.
fonte
O compilador C ++ típico se traduz em código de máquina, não em C ++ . Ele remove parênteses inúteis, sim, porque no momento em que é feito, não há parênteses. O código da máquina não funciona dessa maneira.
fonte
Ambos os códigos acabam codificados como 6:
Verifique você mesmo aqui
fonte
Não, mas sim, mas talvez, mas talvez o contrário, mas não.
Como as pessoas já apontaram, (assumindo uma linguagem em que a adição é associativa à esquerda, como C, C ++, C # ou Java), a expressão
((1 + 2) + 3)
é exatamente equivalente a1 + 2 + 3
. São maneiras diferentes de escrever algo no código-fonte, que teria efeito nulo no código de máquina ou no código de bytes resultante.De qualquer maneira, o resultado será uma instrução para, por exemplo, adicionar dois registradores e, em seguida, adicionar um terceiro, ou pegar dois valores de uma pilha, adicioná-los, empurrá-los para trás, depois pegá-los e outro e adicioná-los ou adicionar três registradores em uma única operação ou outra maneira de somar três números, dependendo do que for mais sensato no próximo nível (o código da máquina ou o código de bytes). No caso do código de bytes, que por sua vez provavelmente passará por uma reestruturação semelhante, por exemplo, o equivalente a IL disso (que seria uma série de cargas para uma pilha e popping pares para adicionar e depois empurrar o resultado). não resultaria em uma cópia direta dessa lógica no nível do código da máquina, mas em algo mais sensato para a máquina em questão.
Mas há algo mais na sua pergunta.
No caso de qualquer compilador C, C ++, Java ou C #, esperaria que o resultado de ambas as instruções fornecidas tivesse exatamente os mesmos resultados que:
Por que o código resultante deve perder tempo fazendo matemática em literais? Nenhuma alteração para o estado do programa irá parar o resultado de
1 + 2 + 3
estar6
, e é isso que deve ir para o código que está sendo executado. De fato, talvez nem isso (dependendo do que você faça com esse 6, talvez possamos jogar tudo fora; e até mesmo o C # com a filosofia de "não otimize fortemente, pois o jitter irá otimizar isso de qualquer maneira" produzirá o equivalenteint a = 6
ou simplesmente jogar tudo fora como desnecessário).Isso nos leva a uma possível extensão de sua pergunta. Considere o seguinte:
e
(Observe, esses dois últimos exemplos não são válidos em C #, enquanto tudo o resto é válido e são válidos em C, C ++ e Java.)
Aqui, novamente, temos código exatamente equivalente em termos de saída. Como não são expressões constantes, não serão calculadas em tempo de compilação. É possível que um formulário seja mais rápido que outro. O que é mais rápido? Isso dependeria do processador e talvez de algumas diferenças arbitrárias de estado (principalmente porque se um é mais rápido, não é provável que seja muito mais rápido).
E elas não estão totalmente relacionadas à sua pergunta, pois tratam principalmente de diferenças na ordem em que algo é feito conceitualmente .
Em cada um deles, há uma razão para suspeitar que um pode ser mais rápido que o outro. Os decréscimos únicos podem ter uma instrução especializada; portanto,
(b / 2)--
podem ser mais rápidos que(b - 2) / 2
.d * 32
talvez pudesse ser produzida mais rapidamente, transformando-ad << 5
assim tornandod * 32 - d
mais rápido do qued * 31
. As diferenças entre os dois últimos são particularmente interessantes; um permite que algum processamento seja ignorado em alguns casos, mas o outro evita a possibilidade de predição incorreta de ramificação.Então, isso nos deixa com duas perguntas: 1. Uma é realmente mais rápida que a outra? 2. Um compilador converterá o mais lento no mais rápido?
E a resposta é 1. Depende. 2. Talvez.
Ou, para expandir, depende porque depende do processador em questão. Certamente existiam processadores em que o equivalente ingênuo do código de máquina de um seria mais rápido que o equivalente ingênuo do código de máquina do outro. Ao longo da história da computação eletrônica, também não houve uma que fosse sempre mais rápida (o elemento de predição incorreta de ramificação em particular não era relevante para muitos quando as CPUs sem pipeline eram mais comuns).
E talvez, porque existem várias otimizações diferentes que os compiladores (e nervosismo e mecanismos de script) farão, e embora alguns possam ser obrigatórios em certos casos, geralmente seremos capazes de encontrar algumas partes de código logicamente equivalente que até o compilador mais ingênuo tem exatamente os mesmos resultados e algumas partes de código logicamente equivalente, onde até o mais sofisticado produz código mais rápido para um do que para o outro (mesmo que tenhamos que escrever algo totalmente patológico apenas para provar nosso argumento).
Não. Mesmo com diferenças mais complicadas do que as que dou aqui, parece uma preocupação absolutamente minuciosa que não tem nada a ver com otimização. Na verdade, é uma questão de pessimização, pois você suspeita que o mais difícil de ler
((1 + 2) + 3
pode ser mais lento do que o mais fácil de ler1 + 2 + 3
.Se realmente era assim que escolher C ++ sobre C # ou Java era "tudo", eu diria que as pessoas deveriam gravar sua cópia do Stroustrup e da ISO / IEC 14882 e liberar o espaço do compilador C ++ para deixar espaço para mais alguns MP3s ou algo assim.
Esses idiomas têm vantagens diferentes entre si.
Uma delas é que o C ++ ainda é geralmente mais rápido e mais leve no uso da memória. Sim, existem exemplos em que C # e / ou Java são mais rápidos e / ou têm melhor uso da memória durante a vida útil do aplicativo, e estes estão se tornando mais comuns à medida que as tecnologias envolvidas melhoram, mas ainda podemos esperar que o programa médio escrito em C ++ seja um executável menor que faz seu trabalho mais rápido e usa menos memória que o equivalente em um desses dois idiomas.
Isso não é otimização.
Às vezes, otimização é usada para significar "tornar as coisas mais rápidas". É compreensível, porque, muitas vezes, quando realmente estamos falando sobre "otimização", estamos realmente falando sobre tornar as coisas mais rápidas e, portanto, uma se tornou uma abreviação para a outra e eu admito que eu uso mal a palavra dessa maneira.
A palavra correta para "acelerar as coisas" não é otimização . A palavra correta aqui é melhoria . Se você fizer uma alteração em um programa e a única diferença significativa é que agora ele é mais rápido, não é otimizado de forma alguma, é apenas melhor.
Otimização é quando fazemos uma melhoria em relação a um aspecto particular e / ou caso particular. Exemplos comuns são:
Tais casos seriam justificados se, por exemplo:
Porém, esses casos também não seriam justificados em outros cenários: o código não foi aprimorado por uma medida infalível absoluta de qualidade, foi aprimorado em um aspecto específico que o torna mais adequado para um uso específico; otimizado.
E a escolha do idioma tem efeito aqui, porque a velocidade, o uso da memória e a legibilidade podem ser afetados por ele, mas também a compatibilidade com outros sistemas, disponibilidade de bibliotecas, disponibilidade de tempos de execução, maturidade desses tempos de execução em um determinado sistema operacional. (pelos meus pecados, de alguma forma, acabei tendo Linux e Android como meus sistemas operacionais favoritos e C # como meu idioma favorito, e enquanto o Mono é ótimo, mas ainda me deparei com isso bastante).
Dizer "escolher C ++ em C # / Java / ... é tudo sobre otimizações" só faz sentido se você acha que C ++ realmente é péssimo, porque a otimização é "melhor apesar de ..." não "melhor". Se você acha que o C ++ é melhor, apesar de tudo, a última coisa que você precisa é se preocupar com possíveis microopções tão minuciosas. De fato, é melhor você provavelmente abandoná-lo; hackers felizes também são uma qualidade para otimizar!
Se, no entanto, você está inclinado a dizer "Eu amo C ++, e uma das coisas que eu amo sobre isso é extrair ciclos extras", então isso é uma questão diferente. Ainda é um caso em que as micro-opções valem a pena se puderem ser um hábito reflexivo (ou seja, a maneira como você tende a codificar naturalmente será mais rápida com mais frequência do que com a velocidade mais lenta). Caso contrário, nem sequer são otimizações prematuras, são pessimizações prematuras que apenas pioram as coisas.
fonte
Parênteses existem para informar ao compilador em que expressões de ordem devem ser avaliadas. Às vezes, são inúteis (exceto que melhoram ou pioram a legibilidade), porque especificam a ordem que seria usada de qualquer maneira. Às vezes eles mudam a ordem. Dentro
praticamente todos os idiomas existentes têm uma regra de que a soma é avaliada adicionando 1 + 2, depois adicionando o resultado mais 3. Se você escreveu
então o parêntese forçaria uma ordem diferente: primeiro adicionando 2 + 3, depois adicionando 1 mais o resultado. Seu exemplo de parênteses produz a mesma ordem que teria sido produzida de qualquer maneira. Agora, neste exemplo, a ordem das operações é um pouco diferente, mas da maneira que a adição de número inteiro funciona, o resultado é o mesmo. Dentro
os parênteses são críticos; deixá-los de fora mudaria o resultado de 9 para 1.
Depois que o compilador determinar quais operações são executadas em qual ordem, os parênteses são completamente esquecidos. Tudo o que o compilador se lembra neste momento é de quais operações executar em qual ordem. Portanto, não há realmente nada que o compilador possa otimizar aqui, os parênteses se foram .
fonte
practically every language in existence
; exceto APL: Tente (aqui) [tryapl.org] digitando(1-2)+3
(2),1-(2+3)
(-4) e1-2+3
(também -4).Eu concordo com muito do que foi dito, no entanto ... o principal aqui é que os parênteses existem para coagir a ordem de operação ... que o compilador está absolutamente fazendo. Sim, produz código de máquina ... mas esse não é o ponto e não é o que está sendo solicitado.
Os parênteses realmente se foram: como já foi dito, eles não fazem parte do código de máquina, que é números e nada mais. O código de montagem não é um código de máquina, é legível por meio de humanos e contém as instruções pelo nome - não pelo código de operação. A máquina executa o que é chamado de opcodes - representações numéricas da linguagem assembly.
Idiomas como Java se enquadram em uma área intermediária, pois são compilados apenas parcialmente na máquina que os produz. Eles são compilados para codificar o código específico da máquina que os executa, mas isso não faz diferença para essa pergunta - os parênteses ainda desaparecem após a primeira compilação.
fonte
a = f() + (g() + h());
, o compilador é livre para chamarf
,g
eh
nessa ordem (ou em qualquer ordem agrada).Compiladores, independentemente do idioma, traduzem toda a matemática do infix para o postfix. Em outras palavras, quando o compilador vê algo como:
traduz para isso:
Isso é feito porque, embora a notação infix seja mais fácil para as pessoas lerem, a notação postfix está muito mais próxima das etapas reais que o computador precisa executar para realizar o trabalho (e porque já existe um algoritmo bem desenvolvido para isso). Por definição, o postfix elimina todos os problemas com ordem de operação ou parênteses, o que naturalmente facilita muito as coisas ao escrever o código da máquina.
Eu recomendo o artigo da wikipedia sobre Notação Polonesa Reversa para obter mais informações sobre o assunto.
fonte