O compilador C ++ remove / otimiza parênteses inúteis?

19

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).

Sarja
fonte
9
Eu acho que C # e Java também otimizarão isso. Eu acredito que quando eles analisam e criam AST, eles apenas removem coisas inúteis óbvias como essa.
Farid Nouri Neshat
5
Tudo o que li aponta para a compilação JIT facilmente, permitindo que a compilação antecipada seja executada pelo seu dinheiro, de modo que isso por si só não é um argumento muito convincente. Você apresenta a programação de jogos - o verdadeiro motivo para favorecer a compilação antecipada é que ela é previsível - com a compilação JIT, você nunca sabe quando o compilador entrará em ação e tentará começar a compilar o código. Mas eu observaria que a compilação antecipada do código nativo não é mutuamente exclusiva com a coleta de lixo, veja, por exemplo, ML e D. padrão. E vi argumentos convincentes de que a coleta de lixo é mais eficiente ...
Doval
7
... do que RAII e ponteiros inteligentes, então é mais uma questão de seguir o caminho mais conhecido (C ++) versus o caminho relativamente não seguido de se fazer programação de jogos nessas linguagens. Eu também observaria que se preocupar com parênteses é insano - vejo de onde você vem, mas é uma micro-otimização ridícula. A escolha de estruturas de dados e algoritmos em seu programa definitivamente dominará o desempenho, não essas trivialidades.
Doval
6
Hum ... Que tipo de otimização você espera exatamente? Se você está falando sobre análise estática, na maioria dos idiomas que conheço, isso será substituído pelo resultado estaticamente conhecido (implementações baseadas em LLVM ainda impõem isso, AFAIK). Se você está falando sobre ordem de execução, não importa, pois é a mesma operação e sem efeitos colaterais. A adição precisa de dois operandos de qualquer maneira. E se você estiver usando isso para comparar C ++, Java e C # em relação ao desempenho, parece que você não tem uma idéia clara do que são as otimizações e como elas funcionam, portanto, concentre-se em aprender isso.
Theodoros Chatzigiannakis
5
Eu me pergunto por que: a) você considera a expressão entre parênteses mais legível (para mim, apenas parece feia, enganosa (por que eles enfatizam essa ordem em particular? Não deveria ser assiciativa aqui?) E desajeitada) b) por que você pensaria sem parênteses, ele pode ter um desempenho melhor (analisar claramente as parênteses é mais fácil para uma máquina do que ter que raciocinar sobre fixações do operador. Como diz Marc van Leuwen, porém, isso não tem nenhuma influência no tempo de execução).
leftaroundabout

Respostas:

87

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).

Marc van Leeuwen
fonte
s / oldes / mais velhos /. Boa resposta, +1.
David Conrad
23
Aviso total de nitpick: "A pergunta não está muito bem colocada". - Discordo, considerando "bem colocado" como "claramente sugerindo qual lacuna de conhecimento o consultor deseja preencher". Em essência, a pergunta é "otimizando para X, escolha A ou B, e por quê? O que acontece por baixo?", O que pelo menos para mim sugere muito claramente qual é a lacuna de conhecimento. A falha na pergunta, que você aponta e aborda muito bem, é que ela se baseia em um modelo mental defeituoso.
Jonas Kölker
Pois 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.
Phil Perry
1
@OrangeDog verdade, é uma pena que o comentário de Ben tenha trazido algumas pessoas que gostam de afirmar que as VMs são mais rápidas que as nativas.
Gbjbaanb
1
@ JonasKölker: Minha frase de abertura na verdade se refere à pergunta formulada no título: não se pode realmente responder a uma pergunta sobre se o compilador insere ou remove parênteses, pois isso se baseia em um equívoco de como os compiladores operam. Mas eu concordo que é bastante claro qual lacuna de conhecimento precisa ser tratada.
Marc van Leeuwen
46

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.

gbjbaanb
fonte
9
Absolutamente - vara como muitos parênteses em como você gosta e deixar o compilador fazer o trabalho duro de descobrir o que você quer :)
gbjbaanb
23
A programação verdadeira do @Serge é mais sobre legibilidade do seu código do que sobre desempenho. Você se odiará no próximo ano, quando precisar depurar uma falha e apenas tiver um código "otimizado".
catraca aberração
1
@ratchetfreak, você está certo, mas eu também sei como comentar meu código. int a = 6; // = (1 + 2) + 3
Serge
20
@Serge eu sei que não irá realizar-se depois de um ano de ajustes, em comentários hora e código vai sair de sincronização e, em seguida, você acabar comint a = 8;// = 2*3 + 5
aberração catraca
21
ou de www.thedailywtf.com:int five = 7; //HR made us change this to six...
Mooing Duck
23

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!

short int a = 30001, b = 30002, c = 30003;
int d = -a + b + c;    // ok
int d = (-a + b) + c;  // ok, same code
int d = (-a + b + c);  // ok, same code
int d = ((((-a + b)) + c));  // ok, same code
int d = -a + (b + c);  // undefined behaviour, different code

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.

david.pfx
fonte
O comportamento indefinido na última linha é porque um curto assinado tem apenas 15 bits após o sinal, portanto, um tamanho máximo de 32767, certo? Nesse exemplo trivial, o compilador deve avisar sobre o excesso, certo? +1 para um exemplo de contador, de qualquer forma. Se eles fossem parâmetros para uma função, você não receberia um aviso. Além disso, se ele apuder realmente ser não assinado, o cálculo do seu cálculo -a + bpoderia facilmente estourar também, se afosse negativo e bpositivo.
Patrick M
@PatrickM: veja a edição. Comportamento indefinido significa que o compilador pode fazer o que quiser, incluindo emitir um aviso ou não. A aritmética não assinada não produz UB, mas reduz o módulo na próxima potência mais alta de dois.
David.pfx
A expressão (b+c)na última linha promoverá seus argumentos para int, portanto, a menos que o compilador defina int16 bits (seja por ser antigo ou direcionado a um pequeno microcontrolador), a última linha seria perfeitamente legítima.
Supercat
@ supercat: Eu acho que não. O tipo comum e o tipo do resultado devem ser int curto. Se isso não é algo que já foi perguntado, talvez você queira postar uma pergunta?
David.pfx
@ david.pfx: As regras das promoções aritméticas em C são bastante claras: tudo menor do que o intpromovido, a intmenos que esse tipo seja incapaz de representar todos os seus valores, caso em que é promovido unsigned 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. Se intfosse um tipo de 16 bits que preso em excesso, no entanto, então não haveria casos em que uma das expressões ...
supercat
7

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 ...

  • "int a = ((1 + 2) + 3);"

... um compilador pode emitir algo mais parecido com isto:

  • Char [1] :: "a"
  • Int32 :: DeclareStackVariable ()
  • Int32 :: 0x00000001
  • Int32 :: 0x00000002
  • Int32 :: Add ()
  • Int32 :: 0x00000003
  • Int32 :: Add ()
  • Int32 :: AssignToVariable ()
  • void :: DiscardResult ()

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!

Phill W.
fonte
4
Não existe um único compilador C ++ que produza algo remotamente parecido com este. Geralmente eles produzem código de CPU real, e a montagem também não parece isso.
MSalters
3
a intenção aqui era demonstrar a diferença na estrutura entre o código conforme escrito e a saída compilada. mesmo aqui a maioria das pessoas não seria capaz de ler código real máquina ou montagem
dhall
O @MSalters Nitpicking clang emitirá "algo assim" se você tratar o LLVM ISA como "algo assim" (o SSA não é baseado em pilha). Dado que é possível escrever o back-end da JVM para o LLVM, o JVM ISA (AFAIK) é baseado em pilha clang-> llvm-> A JVM seria muito semelhante.
Maciej Piechotka
Não acho que o ISL do LLVM tenha a capacidade de definir variáveis ​​de pilha de nomes usando literais de seqüência de caracteres de tempo de execução (apenas as duas primeiras instruções). Isso está seriamente misturando tempo de execução e tempo de compilação. A distinção importa porque esta pergunta é exatamente sobre essa confusão.
MSalters
6

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.

catraca arrepiante
fonte
+1 para mostrar que as operações de ponto flutuante não são associativas.
Doval
No exemplo da pergunta, os parênteses não alteram a associatividade padrão (esquerda); portanto, este ponto é discutível.
Marc van Leeuwen
1
Sobre a última frase, acho que não. Nos java a e c #, o compilador produzirá bytecode / IL otimizado. O tempo de execução não é afetado.
Stefano Altieri
O IL não funciona em expressões desse tipo, as instruções recebem um certo número de valores de uma pilha e retornam um certo número de valores (geralmente 0 ou 1) à pilha. Falar sobre esse tipo de coisa sendo otimizado em tempo de execução em C # é um absurdo.
9137 Jon Hanna
6

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.

The Spooniest
fonte
5

Ambos os códigos acabam codificados como 6:

movl    $6, -4(%rbp)

Verifique você mesmo aqui

MonoThreaded
fonte
2
O que é esse assembly.ynh.io ?
Peter Mortensen
É um compilador pseudo que o código transforma C em x86 assembly line
MonoThreaded
1

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 a 1 + 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:

int a = 6;

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 + 3estar 6, 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 equivalente int a = 6ou simplesmente jogar tudo fora como desnecessário).

Isso nos leva a uma possível extensão de sua pergunta. Considere o seguinte:

int a = (b - 2) / 2;
/* or */
int a = (b / 2)--;

e

int c;
if(d < 100)
  c = 0;
else
  c = d * 31;
/* or */
int c = d < 100 ? 0 : d * 32 - d
/* or */
int c = d < 100 && d * 32 - d;
/* or */
int c = (d < 100) * (d * 32 - d);

(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 * 32talvez pudesse ser produzida mais rapidamente, transformando-a d << 5assim tornando d * 32 - dmais rápido do que d * 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).

Pode parecer uma preocupação muito pequena de otimização,

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) + 3pode ser mais lento do que o mais fácil de ler 1 + 2 + 3.

mas escolher C ++ em C # / Java / ... tem tudo a ver com otimizações (IMHO).

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:

  1. Agora é mais rápido para um caso de uso, mas mais lento para outro.
  2. Agora é mais rápido, mas usa mais memória.
  3. Agora está mais claro na memória, mas mais lento.
  4. Agora é mais rápido, mas mais difícil de manter.
  5. Agora é mais fácil de manter, mas mais lento.

Tais casos seriam justificados se, por exemplo:

  1. O caso de uso mais rápido é mais comum ou mais dificultado para começar.
  2. O programa era inaceitavelmente lento e temos muita memória livre.
  3. O programa estava paralisado porque usava tanta RAM que passava mais tempo trocando do que executando seu processamento super-rápido.
  4. O programa era inaceitavelmente lento e o código mais difícil de entender é bem documentado e relativamente estável.
  5. O programa ainda é aceitável e rápido, e a base de código mais compreensível é mais barata de manter e permite que outras melhorias sejam feitas com mais facilidade.

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.

Jon Hanna
fonte
0

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

int a = 1 + 2 + 3;

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

int a = 1 + (2 + 3);

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

int a = 10 - (5 - 4);

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 .

gnasher729
fonte
practically every language in existence; exceto APL: Tente (aqui) [tryapl.org] digitando (1-2)+3(2), 1-(2+3)(-4) e 1-2+3(também -4).
tomsmeding 13/05
0

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.

jinzai
fonte
1
Não tenho certeza de que isso responda à pergunta. Os parágrafos de apoio são mais confusos do que úteis. Como o compilador Java é relevante para o compilador C ++?
Adam Zuckerman
O OP perguntou se os parênteses se foram… Eu disse que eles estavam e expliquei ainda que o código executável são apenas números que representam opcodes. Java foi criado em outra resposta. Eu acho que responde bem à pergunta ... mas essa é apenas a minha opinião. Obrigado por responder.
Jinzai
3
Parênteses não forçam "ordem de operação". Eles mudam de precedência. Assim, em a = f() + (g() + h());, o compilador é livre para chamar f, ge hnessa ordem (ou em qualquer ordem agrada).
Alok
Eu tenho alguma discordância com essa afirmação ... você absolutamente pode coagir a ordem de operação entre parênteses.
Jinzai #
0

Compiladores, independentemente do idioma, traduzem toda a matemática do infix para o postfix. Em outras palavras, quando o compilador vê algo como:

((a+b)+c)

traduz para isso:

 a b + c +

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.

Brian Drozd
fonte
5
Essa é uma suposição incorreta sobre como os compiladores traduzem operações. Você está assumindo, por exemplo, uma máquina de empilhar aqui. E se você tivesse um processador vetorial? e se você tivesse uma máquina com uma grande quantidade de registros?
Ahmed Masud