Operador Evitar incremento do Postfix

25

Li que devo evitar o operador de incremento do postfix por motivos de desempenho (em certos casos).

Mas isso não afeta a legibilidade do código? Na minha opinião:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

Parece melhor que:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

Mas isso provavelmente é apenas por hábito. É certo que não tenho visto muita utilidade ++i.

O desempenho é tão ruim para sacrificar a legibilidade, neste caso? Ou sou apenas cego e ++ié mais legível do que i++?

Mateen Ulhaq
fonte
1
Eu usei i++antes de saber que isso poderia afetar o desempenho ++i, então mudei. No começo, o último parecia um pouco estranho, mas depois de um tempo eu me acostumei e agora parece tão natural quanto i++.
gablin
15
++ie i++faça coisas diferentes em certos contextos, não assuma que são iguais.
Orbling 19/03/11
2
É sobre C ou C ++? São duas línguas muito diferentes! :-) Em C ++, o loop for idiomático é for (type i = 0; i != 42; ++i). Não apenas pode operator++ser sobrecarregado, mas também pode operator!=e operator<. O incremento de prefixo não é mais caro que o postfix, diferente de não é mais caro que menos de. Quais devemos usar?
Bo Persson
7
Não deveria ser chamado ++ C?
Armand
21
@ Stephen: C ++ significa pegar C, adicionar a ele e depois usar o antigo .
Supercat

Respostas:

58

Os fatos:

  1. i ++ e ++ i são igualmente fáceis de ler. Você não gosta de um porque não está acostumado a isso, mas não há essencialmente nada que possa interpretá-lo como errado, portanto, não há mais trabalho de ler ou escrever.

  2. Em pelo menos alguns casos, o operador postfix será menos eficiente.

  3. No entanto, em 99,99% dos casos, não importa porque (a) estará agindo de um modo simples ou primitivo de qualquer maneira e é apenas um problema se estiver copiando um objeto grande (b) não estará em um desempenho parte crítica do código (c) você não sabe se o compilador irá otimizá-lo ou não, isso pode acontecer.

  4. Portanto, sugiro o uso do prefixo, a menos que você precise especificamente do postfix, seja um bom hábito, apenas porque (a) é um bom hábito ser preciso com outras coisas e (b) uma vez na lua azul, você pretende usar o postfix e entenda errado: se você sempre escreve o que quer dizer, é menos provável. Sempre há uma troca entre desempenho e otimização.

Você deve usar seu bom senso e não otimizar micro até que você precise, mas nem ser flagrantemente ineficiente por causa disso. Normalmente, isso significa: primeiro, exclua qualquer construção de código que seja inaceitavelmente ineficiente, mesmo em código não crítico de tempo (normalmente algo que representa um erro conceitual fundamental, como passar objetos de 500 MB por valor sem motivo); e segundo, de qualquer outra maneira de escrever o código, escolha a mais clara.

No entanto, aqui, acredito que a resposta é simples: acredito que escrever prefixo, a menos que você precise especificamente do postfix seja (a) muito marginalmente mais claro e (b) muito marginalmente mais propenso a ser mais eficiente; portanto, você sempre deve escrever isso por padrão, mas não se preocupe se você esquecer.

Seis meses atrás, pensei o mesmo que você, que o i ++ era mais natural, mas é exatamente o que você está acostumado.

EDIÇÃO 1: Scott Meyers, em "C ++ mais eficaz", em que geralmente confio nessa coisa, diz que você geralmente deve evitar o uso do operador postfix em tipos definidos pelo usuário (porque a única implementação sadia da função de incremento do postfix é fazer um cópia do objeto, chame a função de incremento de prefixo para executar o incremento e retorne a cópia, mas as operações de cópia podem ser caras).

Portanto, não sabemos se existem regras gerais sobre (a) se isso é verdade hoje, (b) se isso também se aplica (menos) aos tipos intrínsecos (c) se você deve usar "++" em qualquer coisa além de uma classe de iterador leve de todos os tempos. Mas, por todos os motivos que descrevi acima, não importa, faça o que eu disse antes.

EDIT 2: Refere-se à prática geral. Se você acha que isso importa em algum exemplo específico, deve analisá-lo e ver. A criação de perfil é fácil, barata e funciona. Deduzir, desde os primeiros princípios, o que precisa ser otimizado é difícil, caro e não funciona.

Jack V.
fonte
Sua postagem está certa. Nas expressões em que o operador infix + e o pós-incremento ++ foram sobrecarregados, como aClassInst = someOtherClassInst + yetAnotherClassInst ++, o analisador gerará código para executar a operação aditiva antes de gerar o código para executar a operação pós-incremento, aliviando a necessidade de crie uma cópia temporária. O assassino de desempenho aqui não é pós-incremento. É o uso de um operador infix sobrecarregado. Operadores Infix produzem novas instâncias.
bit-twiddler
2
I altamente suspeito que a razão as pessoas são 'usado' para i++, em vez de ++ié por causa do nome de uma determinada linguagem de programação popular, referenciado neste pergunta / resposta ...
Sombra
61

Sempre codifique primeiro o programador e o computador segundo.

Se houver uma diferença de desempenho, depois que o compilador lançar seu olhar de especialista sobre o seu código, E você pode medi-lo E isso é importante - então você pode alterá-lo.

Martin Beckett
fonte
7
Declaração soberba !!!
19411 Dave
8
@ Martin: é exatamente por isso que eu usaria o incremento de prefixo. A semântica do Postfix implica manter o valor antigo por perto e, se não houver necessidade, é impreciso usá-lo.
Matthieu M. 20/03
1
Para um índice de loop que seria mais claro - mas se você fosse iteração sobre um array incrementando um ponteiro e uso prefixo significava a partir de um único endereço ilegal antes do início que seria ruim, independentemente de um aumento de desempenho
Martin Beckett
5
@ Matthew: Simplesmente não é verdade que o pós-incremento implique em manter uma cópia do valor antigo. Não se pode ter certeza de como um compilador lida com valores intermediários até visualizar sua saída. Se você dedicar algum tempo para exibir minha lista de linguagem de assembly gerada pelo GCC anotada, verá que o GCC gera o mesmo código de máquina para os dois loops. Esse absurdo sobre favorecer o pré-incremento ao pós-incremento, porque é mais eficiente, é pouco mais que conjectura.
precisa saber é o seguinte
2
@ Mathhieu: O código que publiquei foi gerado com a otimização desativada. A especificação C ++ não indica que um compilador deve produzir uma instância temporária de um valor quando o pós-incremento é usado. Apenas indica a precedência dos operadores de pré e pós-incremento.
precisa saber é o seguinte
13

O GCC produz o mesmo código de máquina para os dois loops.

Código C

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

Código de montagem (com meus comentários)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols
bit-twiddler
fonte
Que tal com a otimização ativada?
serv-inc
2
@ usuário: Provavelmente nenhuma mudança, mas você realmente espera que o bit-twiddler volte tão cedo?
Deduplicator
2
Cuidado: enquanto em C não há tipos definidos pelo usuário com operadores sobrecarregados, em C ++ existem e a generalização de tipos básicos para tipos definidos pelo usuário é simplesmente inválida .
Deduplicator
@ Reduplicador: Obrigado, também por apontar que esta resposta não generaliza para tipos definidos pelo usuário. Eu não tinha olhado para sua página de usuário antes de perguntar.
serv-inc
12

Não se preocupe com o desempenho, digamos, 97% do tempo. Otimização prematura é a raiz de todo o mal.

- Donald Knuth

Agora que isso está fora do nosso caminho, vamos fazer nossa escolha de maneira sutil :

  • ++i: incremento de prefixo , incrementa o valor atual e produz o resultado
  • i++: incremento postfix , copie o valor, incremente o valor atual, produz a cópia

A menos que seja necessária uma cópia do valor antigo, o uso do incremento do postfix é uma maneira completa de fazer as coisas.

A imprecisão vem da preguiça; sempre use a construção que expressa sua intenção da maneira mais direta; há menos chances do que o futuro mantenedor possa entender mal sua intenção original.

Embora seja (realmente) menor aqui, há momentos em que fico realmente intrigado ao ler o código: fiquei realmente imaginando se a intenção e o expresso real coincidiam e, é claro, depois de alguns meses, eles (ou eu) também não se lembrava ...

Portanto, não importa se parece certo para você ou não. Abrace o BEIJO . Em alguns meses, você evitará suas práticas antigas.

Matthieu M.
fonte
4

Em C ++, você poderia fazer uma diferença substancial de desempenho se houver sobrecargas de operador envolvidas, especialmente se estiver escrevendo código modelado e não souber em que iteradores podem ser transmitidos. A lógica por trás de qualquer iterador X pode ser substancial e significativa- isto é, lento e não otimizável pelo compilador.

Mas esse não é o caso em C, onde você sabe que será apenas um tipo trivial, e a diferença de desempenho é trivial e o compilador pode facilmente otimizar.

Uma dica: você programa em C ou em C ++ e as perguntas estão relacionadas a um ou outro, não a ambos.

DeadMG
fonte
2

O desempenho de qualquer operação é altamente dependente da arquitetura subjacente. É preciso incrementar um valor armazenado na memória, o que significa que o gargalo de von Neumann é o fator limitante nos dois casos.

No caso de ++ i, temos que

Fetch i from memory 
Increment i
Store i back to memory
Use i

No caso do i ++, temos que

Fetch i from memory
Use i
Increment i
Store i back to memory

Os operadores ++ e - rastreiam sua origem no conjunto de instruções PDP-11. O PDP-11 pode executar pós-incremento automático em um registro. Também poderia executar um pré-decréscimo automático em um endereço efetivo contido em um registro. Em qualquer um dos casos, o compilador poderia tirar vantagem dessas operações no nível da máquina se a variável em questão fosse uma variável "register".

bit-twiddler
fonte
2

Se você quiser saber se algo está lento, teste-o. Pegue um BigInteger ou equivalente, coloque-o em um loop for similar usando os dois idiomas, verifique se o interior do loop não fica otimizado e cronometre os dois.

Depois de ler o artigo, não o acho muito convincente, por três razões. Primeiro, o compilador deve ser capaz de otimizar a criação de um objeto que nunca é usado. Segundo, o i++conceito é idiomático para numérico para loops , de modo que os casos em que posso ver realmente sendo afetados são limitados. Terceiro, eles fornecem um argumento puramente teórico, sem números para apoiá-lo.

Com base no motivo nº 1, especialmente, meu palpite é que, quando você realmente fizer o tempo, eles estarão próximos um do outro.

jprete
fonte
-1

Antes de tudo, isso não afeta a legibilidade da IMO. Não é o que você está acostumado a ver, mas demoraria apenas um pouco para você se acostumar.

Segundo, a menos que você use uma tonelada de operadores de postfix no seu código, provavelmente não verá muita diferença. O principal argumento para não usá-los quando possível é que uma cópia do valor do var original deve ser mantida até o final dos argumentos em que o var original ainda pode ser usado. Isso significa 32 bits ou 64 bits, dependendo da arquitetura. Isso equivale a 4 ou 8 bytes ou 0,00390625 ou 0,0078125 MB. As chances são muito altas de que, a menos que você esteja usando uma tonelada delas, que precisam ser salvas por um período muito longo, com os recursos e a velocidade atuais dos computadores, você nem notaria diferença ao mudar do postfix para o prefixo.

Edição: Esqueça esta parte restante, pois minha conclusão foi provada falsa (exceto para a parte de ++ iei ++ nem sempre fazendo a mesma coisa ... isso ainda é verdade).

Também foi apontado anteriormente que eles não fazem a mesma coisa em alguns casos. Tenha cuidado ao fazer a troca, se você decidir. Eu nunca experimentei (sempre usei o postfix), por isso não tenho certeza, mas acho que mudar do postfix para o prefixo resultará em resultados diferentes: (novamente eu posso estar errado ... depende do compilador / intérprete também)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}
Kenneth
fonte
4
A operação de incremento ocorre no final do loop for, portanto, eles teriam exatamente a mesma saída. Não depende do compilador / intérprete.
jsternberg
@jsternberg ... Obrigado, eu não tinha certeza de quando o incremento aconteceu, pois nunca tive um motivo para testá-lo. Faz muito tempo desde que fiz compiladores na faculdade! lol
Kenneth
Errado Errado Errado.
ruohola
-1

Eu acho que semanticamente, ++ifaz mais sentido do que i++, então eu continuaria com o primeiro, exceto que é comum não fazer isso (como em Java, onde você deve usar i++porque é amplamente usado).

Oliver Weiler
fonte
-2

Não se trata apenas de desempenho.

Às vezes, você deseja evitar a implementação de cópias, porque não faz sentido. E como o uso do incremento do prefixo não depende disso, é mais simples manter o formato do prefixo.

E usar diferentes incrementos para tipos primitivos e complexos ... isso é realmente ilegível.

maxim1000
fonte
-2

A menos que você realmente precise, eu continuaria com o ++ i. Na maioria dos casos, é isso que se pretende. Não é com muita frequência que você precisa do i ++ e sempre precisa pensar duas vezes ao ler essa construção. Com o ++ i, é fácil: você adiciona 1, usa-o e então eu continuo o mesmo.

Portanto, concordo plenamente com o @martin beckett: facilite para si mesmo, já é difícil o suficiente.

Peter Frings
fonte