Evite el operador de incremento de Postfix

25

He leído que debería evitar el operador de incremento de postfix debido a razones de rendimiento (en ciertos casos).

¿Pero esto no afecta la legibilidad del código? En mi opinión:

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

Se ve mejor que:

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

Pero esto es probablemente solo por costumbre. Es cierto que no he visto muchos usos ++i.

¿Es el rendimiento tan malo para sacrificar la legibilidad, en este caso? ¿O simplemente soy ciego y ++ies más legible que i++?

Mateen Ulhaq
fuente
1
Lo usé i++antes de saber que podría afectar el rendimiento ++i, así que cambié. Al principio, este último parecía un poco extraño, pero después de un tiempo me acostumbré y ahora se siente tan natural como i++.
gablin
15
++iy i++hacer cosas diferentes en ciertos contextos, no asuma que son lo mismo.
Orbling
2
¿Se trata de C o C ++? ¡Son dos idiomas muy diferentes! :-) En C ++, el bucle for idiomático es for (type i = 0; i != 42; ++i). No solo se operator++puede sobrecargar, sino que también se puede operator!=y operator<. El incremento de prefijo no es más caro que el postfix, no igual no es más costoso que menor. ¿Cuáles debemos usar?
Bo Persson
77
¿No debería llamarse ++ C?
Armand
21
@Stephen: C ++ significa tomar C, agregarlo y luego usar el anterior .
supercat

Respuestas:

58

Los hechos:

  1. i ++ y ++ i son igualmente fáciles de leer. No te gusta uno porque no estás acostumbrado, pero esencialmente no hay nada que puedas malinterpretar, así que ya no es más trabajo leer o escribir.

  2. En al menos algunos casos, el operador de postfix será menos eficiente.

  3. Sin embargo, en el 99.99% de los casos, no importará porque (a) de todos modos actuará en un tipo simple o primitivo y solo es un problema si está copiando un objeto grande (b) no estará en un rendimiento parte crítica del código (c) no sabe si el compilador lo optimizará o no, puede que lo haga.

  4. Por lo tanto, sugiero que usar prefijo a menos que necesite específicamente postfix es un buen hábito para entrar, solo porque (a) es un buen hábito para ser preciso con otras cosas y (b) una vez en una luna azul, tendrá la intención de usar postfix y dígalo al revés: si siempre escribe lo que quiere decir, es menos probable. Siempre hay una compensación entre rendimiento y optimización.

Debes usar tu sentido común y no micro-optimizar hasta que lo necesites, pero tampoco ser flagrantemente ineficiente por el simple hecho de hacerlo. Por lo general, esto significa: en primer lugar, descartar cualquier construcción de código que sea inaceptablemente ineficiente incluso en un código que no sea crítico para el tiempo (normalmente algo que representa un error conceptual fundamental, como pasar objetos de 500 MB por valor sin ningún motivo); y segundo, de cualquier otra forma de escribir el código, elija el más claro.

Sin embargo, creo que la respuesta es simple: creo que escribir un prefijo a menos que necesite específicamente postfix es (a) muy marginalmente más claro y (b) muy marginalmente más probable que sea más eficiente, por lo que siempre debe escribirlo de forma predeterminada, pero No te preocupes si lo olvidas.

Hace seis meses, pensé lo mismo que tú, que i ++ era más natural, pero es puramente a lo que estás acostumbrado.

EDITAR 1: Scott Meyers, en "C ++ más eficaz", en quien generalmente confío en esto, dice que en general debe evitar usar el operador postfix en tipos definidos por el usuario (porque la única implementación sensata de la función de incremento de postfix es hacer un copia del objeto, llame a la función de incremento de prefijo para realizar el incremento y devuelva la copia, pero las operaciones de copia pueden ser costosas).

Por lo tanto, no sabemos si existen reglas generales sobre (a) si eso es cierto hoy, (b) si también se aplica (menos) a los tipos intrínsecos (c) si debería usar "++" en nada más que una clase de iterador ligero. Pero por todas las razones que describí anteriormente, no importa, haga lo que dije antes.

EDIT 2: Esto se refiere a la práctica general. Si crees que sí importa en algún caso específico, entonces debes perfilarlo y verlo. Perfilar es fácil y barato y funciona. Deducir de los primeros principios lo que debe optimizarse es difícil y costoso y no funciona.

Jack V.
fuente
Su publicación es correcta en el dinero. En expresiones en las que el operador infijo + y el post-incremento ++ se han sobrecargado, como aClassInst = someOtherClassInst + yetAnotherClassInst ++, el analizador generará código para realizar la operación aditiva antes de generar el código para realizar la operación de post-incremento, aliviando la necesidad de crear una copia temporal El asesino de rendimiento aquí no es post-incremento. Es el uso de un operador infijo sobrecargado. Los operadores de infijo producen nuevas instancias.
bit-twiddler
2
Yo altamente sospechoso que la razón por la gente es 'usado' a i++más que ++ies debido al nombre de un determinado lenguaje de programación muy popular hace referencia en esta pregunta / respuesta ...
Sombra
61

Siempre codifique para el programador primero y la computadora en segundo lugar.

Si hay una diferencia de rendimiento, después de que el compilador haya echado un ojo experto sobre su código, Y puede medirlo Y es importante, entonces puede cambiarlo.

Martin Beckett
fuente
77
Excelente declaración!
Dave
8
@ Martin: que es exactamente por qué usaría el incremento de prefijo. La semántica de postfix implica mantener el antiguo valor, y si no es necesario, entonces es incorrecto usarlo.
Matthieu M.
1
Para un índice de bucle que sería más claro, pero si estuviera iterando sobre una matriz al incrementar un puntero y usar un prefijo significaba comenzar en una dirección ilegal una antes del inicio que sería malo independientemente de un aumento de rendimiento
Martin Beckett
55
@Matthew: Simplemente no es cierto que el incremento posterior implica mantener una copia del valor anterior. Uno no puede estar seguro de cómo un compilador maneja los valores intermedios hasta que uno ve su salida. Si se toma el tiempo de ver mi listado anotado de lenguaje ensamblador generado por GCC, verá que GCC genera el mismo código de máquina para ambos bucles. Esta tontería acerca de favorecer el pre-incremento sobre el post-incremento porque es más eficiente es poco más que una conjetura.
bit-twiddler
2
@Mathhieu: el código que publiqué se generó con la optimización desactivada. La especificación C ++ no establece que un compilador debe producir una instancia temporal de un valor cuando se utiliza el incremento posterior. Simplemente establece la precedencia de los operadores de pre y post incremento.
bit-twiddler
13

GCC produce el mismo código de máquina para ambos bucles.

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 Asamblea (con mis comentarios)

    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
fuente
¿Qué tal con la optimización activada?
serv-inc
2
@usuario: Probablemente no haya cambios, pero ¿realmente espera que Bit-twiddler regrese pronto?
Deduplicador
2
Tenga cuidado: mientras que en C no hay tipos definidos por el usuario con operadores sobrecargados, en C ++ los hay, y la generalización de tipos básicos a tipos definidos por el usuario simplemente no es válida .
Deduplicador
@Deduplicator: Gracias, también por señalar que esta respuesta no se generaliza a los tipos definidos por el usuario. No había mirado su página de usuario antes de preguntar.
serv-inc
12

No se preocupe por el rendimiento, digamos el 97% del tiempo. La optimización prematura es la fuente de todos los males.

- Donald Knuth

Ahora que esto está fuera de nuestro camino, hagamos nuestra elección con sensatez :

  • ++i: incrementa el prefijo , incrementa el valor actual y produce el resultado
  • i++: incremento de postfix , copia el valor, incrementa el valor actual, produce la copia

A menos que se requiera una copia del valor anterior, el uso del incremento de postfix es una forma práctica de hacer las cosas.

La imprecisión proviene de la pereza, siempre use la construcción que expresa su intención de la manera más directa, hay menos posibilidades de que el futuro mantenedor pueda malinterpretar su intención original.

A pesar de que es (realmente) menor aquí, hay momentos en que me ha intrigado mucho leer el código: realmente me preguntaba si la intención y el expreso real coincidieron y, por supuesto, después de unos meses, ellos (o yo) tampoco lo recordaba ...

Por lo tanto, no importa si te parece bien o no. Abrazo BESO . En unos meses habrás evitado tus viejas prácticas.

Matthieu M.
fuente
4

En C ++, tu podrías hacer una diferencia sustancial de rendimiento si hay sobrecargas del operador involucradas, especialmente si está escribiendo código con plantilla y no sabe qué iteradores podrían pasarse. La lógica detrás de cualquier iterador X puede ser sustancial y significativa. es decir, lento e inoptimizable por el compilador.

Pero este no es el caso en C, donde sabe que solo será un tipo trivial, y la diferencia de rendimiento es trivial y el compilador puede optimizar fácilmente.

Un consejo: programa en C, o en C ++, y las preguntas se relacionan con uno u otro, no con ambos.

DeadMG
fuente
2

El rendimiento de cualquiera de las operaciones depende en gran medida de la arquitectura subyacente. Uno tiene que incrementar un valor que se almacena en la memoria, lo que significa que el cuello de botella de von Neumann es el factor limitante en ambos casos.

En el caso de ++ i, tenemos que

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

En el caso de i ++, tenemos que

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

Los operadores ++ y - rastrean su origen al conjunto de instrucciones PDP-11. El PDP-11 podría realizar un post-incremento automático en un registro. También podría realizar un decremento previo automático en una dirección efectiva contenida en un registro. En cualquier caso, el compilador solo podría aprovechar estas operaciones a nivel de máquina si la variable en cuestión fuera una variable de "registro".

bit-twiddler
fuente
2

Si quieres saber si algo es lento, pruébalo. Tome un BigInteger o equivalente, péguelo en un bucle similar usando ambos modismos, asegúrese de que el interior del bucle no se optimice y cronometre ambos.

Después de leer el artículo, no me parece muy convincente, por tres razones. Uno, el compilador debería poder optimizar alrededor de la creación de un objeto que nunca se usa. Dos, el i++concepto es idiomático para los bucles numéricos , por lo que los casos que puedo ver realmente afectados se limitan a. Tres, proporcionan un argumento puramente teórico, sin números que lo respalden.

Sobre la base de la razón n. ° 1 especialmente, supongo que cuando realices el tiempo, estarán uno al lado del otro.

jprete
fuente
-1

En primer lugar, no afecta la legibilidad de la OMI. No es lo que estás acostumbrado a ver, pero solo pasará un tiempo antes de que te acostumbres.

En segundo lugar, a menos que use una tonelada de operadores de postfix en su código, es probable que no vea mucha diferencia. El argumento principal para no usarlos cuando sea posible es que se debe mantener una copia del valor de la var original hasta el final de los argumentos donde la var original todavía podría usarse. Eso es 32 bits o 64 bits dependiendo de la arquitectura. Eso equivale a 4 u 8 bytes o 0.00390625 o 0.0078125 MB. Hay muchas posibilidades de que, a menos que esté utilizando una tonelada de ellas que deba guardarse durante un período de tiempo muy largo, con los recursos y la velocidad de la computadora de hoy en día, ni siquiera notará la diferencia al cambiar de postfix a prefijo.

EDITAR: Olvide esta porción restante, ya que mi conclusión resultó ser falsa (excepto por la parte de ++ i e i ++ que no siempre hacen lo mismo ... eso sigue siendo cierto).

También se señaló anteriormente que no hacen lo mismo en algunos casos. Tenga cuidado al hacer el cambio si así lo decide. Nunca lo he probado (siempre he usado postfix), así que no lo sé con certeza, pero creo que cambiar de postfix a prefijo dará como resultado diferentes resultados: (nuevamente podría estar equivocado ... depende del compilador / intérprete también)

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
fuente
44
La operación de incremento ocurre al final del ciclo for, por lo que tendrían exactamente la misma salida. No depende del compilador / intérprete.
jsternberg
@jsternberg ... Gracias, no estaba seguro de cuándo sucedió el incremento, ya que nunca tuve una razón para probarlo. ¡Ha pasado demasiado tiempo desde que hice compiladores en la universidad! jajaja
Kenneth
Mal mal mal mal.
Ruohola
-1

Creo que semánticamente ++itiene más sentido que i++eso, así que me quedaría con el primero, excepto que es común no hacerlo (como en Java, donde debes usari++ porque es muy usado).

Oliver Weiler
fuente
-2

No se trata solo de rendimiento.

A veces desea evitar implementar la copia, porque no tiene sentido. Y dado que el uso del incremento de prefijo no depende de esto, es más simple apegarse a la forma de prefijo.

Y usar diferentes incrementos para tipos primitivos y tipos complejos ... eso es realmente ilegible.

maxim1000
fuente
-2

A menos que realmente lo necesite, me quedaría con ++ i. En la mayoría de los casos, esto es lo que se pretende. No es muy frecuente que necesite i ++, y siempre tiene que pensarlo dos veces al leer una construcción de este tipo. Con ++ i, es fácil: agregas 1, lo usas y luego sigo siendo el mismo.

Entonces, estoy totalmente de acuerdo con @martin beckett: hazlo más fácil para ti, ya es bastante difícil.

Peter Frings
fuente