performance - the - Instrucción INC vs ADD 1: ¿Importa?
importing into the united states a guide for commercial importers (2)
En su mayoría, me mantengo alejado de INC y DEC ahora, porque hacen actualizaciones de código de condición parcial, y esto puede causar paradas divertidas en la tubería, y ADD / SUB no lo hacen. Entonces, donde no importa (la mayoría de los lugares), uso ADD / SUB para evitar los puestos. Utilizo INC / DEC solo cuando mantengo el código pequeño, por ejemplo, ajustando una línea de caché donde el tamaño de una o dos instrucciones hace la diferencia suficiente para que importe. Probablemente esto sea una nano [¡literalmente!] Optimización sin sentido, pero soy bastante viejo en mis hábitos de codificación.
autor: @Ira Baxter
El fragmento anterior proviene de por qué las instrucciones INC y DEC no afectan la bandera de acarreo.
Y me gustaría preguntar por qué puede causar puestos en la tubería mientras que agregar no? Después de todo, tanto las actualizaciones add como las inc registran registros. La única diferencia es que inc no actualiza CF. Pero, ¿por qué es importante?
Dependiendo de la implementación de la CPU de las instrucciones, una actualización de registro parcial puede causar un bloqueo. De acuerdo con la guía de optimización de Agner Fog, página 62 ,
Por razones históricas, las instrucciones
INC
yDEC
dejan sin cambios la bandera de acarreo, mientras que las otras banderas aritméticas se escriben en. Esto causa una dependencia falsa del valor anterior de los indicadores y cuesta un μop extra. Para evitar estos problemas, se recomienda utilizar siempreADD
ySUB
lugar deINC
yDEC
. Por ejemplo,INC EAX
debe reemplazarse porADD EAX,1
.
Consulte también la página 83 en "Puestos de banderas parciales" y la página 100 en "Puestos de banderas parciales".
En las CPU modernas, add
nunca es más lento que inc
(excepto para los efectos indirectos de tamaño de código / decodificación), pero generalmente tampoco es más rápido, por lo que debería preferir inc
por razones de tamaño de código . Especialmente si esta opción se repite muchas veces en el mismo binario (por ejemplo, si usted es un compilador-escritor).
inc
ahorra 1 byte (modo de 64 bits) o 2 bytes (códigos de operación 0x40..F inc r32
r32 / dec r32
forma abreviada en modo de 32 bits, reutilizado como el prefijo REX para x86-64). Esto hace una pequeña diferencia porcentual en el tamaño total del código. Esto ayuda a las tasas de aciertos de la memoria caché de instrucciones, la tasa de aciertos de iTLB y la cantidad de páginas que deben cargarse desde el disco.
Ventajas de inc
:
- código de tamaño directamente
- No usar un inmediato puede tener efectos de uop-cache en Sandybridge-family, lo que podría compensar la mejor microfusión de
add
. (Consulte la tabla 9.1 de Agner Fog en la sección de Sandybridge de su guía de microarch ). Los contadores de rendimiento pueden medir fácilmente los uops de la etapa de problemas, pero es más difícil medir cómo las cosas se acumulan en la memoria caché uop y en los efectos de ancho de banda de lectura de caché. - Dejar CF sin modificar es una ventaja en algunos casos, en las CPU donde se puede leer CF después de
inc
sin un bloqueo. (No en Nehalem y antes)
Existe una excepción entre las CPU modernas: Silvermont / Goldmont / Knight''s Landing decodifica inc
/ dec
eficiente como 1 uop, pero se expande a 2 en la etapa de asignación / cambio de nombre (también conocido como problema). El uop extra fusiona banderas parciales. inc
rendimiento de inc
es solo 1 por reloj, frente a 0.5c (o 0.33c Goldmont) para add r32, imm8
debido a la cadena de add r32, imm8
creada por los uops de fusión de banderas.
A diferencia de P4, el resultado del registro no tiene un false-dep on flags (ver a continuación), por lo que la ejecución fuera de orden toma la fusión del indicador de la ruta crítica de latencia cuando nada usa el resultado de la bandera. (Pero la ventana OOO es mucho más pequeña que las CPU convencionales como Haswell o Ryzen). Ejecutar inc
como 2 uops separados probablemente sea una victoria para Silvermont en la mayoría de los casos; la mayoría de las instrucciones x86 escriben todas las banderas sin leerlas, rompiendo estas cadenas de dependencia de banderas.
SMont / KNL tiene una cola entre decodificar y asignar / renombrar (consulte el manual de optimización de Intel, figura 16-2 ), por lo que expandir a 2 uops durante la emisión puede llenar burbujas de puestos de decodificación (en instrucciones como one-operando mul
o pshufb
, que producen más de 1 uop del decodificador y causa un bloqueo de 3-7 ciclos para microcódigo). O en Silvermont, solo una instrucción con más de 3 prefijos (incluidos los bytes de escape y los prefijos obligatorios), por ejemplo, REX + cualquier instrucción SSSE3 o SSE4. Pero tenga en cuenta que hay un búfer de bucles ~ 28 uop, por lo que los bucles pequeños no sufren de estos puestos de decodificación.
inc
/ dec
no son las únicas instrucciones que decodifican como 1 sino que emiten como 2: push
/ pop
, call
/ ret
, y lea
con 3 componentes hacen esto también. Entonces, ¿AVX512 de KNL reúne instrucciones? Fuente: manual de optimización de Intel , motor fuera de servicio 17.1.2 (KNL). Es solo una pequeña penalización de rendimiento (y a veces ni siquiera eso si algo más es un cuello de botella más grande), por lo que generalmente está bien usar inc
para afinación "genérica".
El manual de optimización de Intel aún recomienda add 1
sobre inc
en general, para evitar riesgos de paradas de bandera parcial. Pero dado que el compilador de Intel no hace eso de forma predeterminada, no es muy probable que las CPU futuras disminuyan la inc
en todos los casos, como lo hizo P4.
Clang 5.0 e ICC 17 de Intel (en Godbolt) utilizan inc
al optimizar la velocidad ( -O3
), no solo por tamaño. -mtune=pentium4
hace que eviten inc
/ dec
, pero el valor predeterminado -mtune=generic
no le da demasiado peso a P4.
ICC17 -xMIC-AVX512
(equivalente a gcc''s -march=knl
) evita inc
, que probablemente sea una buena apuesta en general para Silvermont / KNL. Pero no suele ser un desastre de rendimiento el uso de inc
, por lo que probablemente aún sea apropiado para la sintonización "genérica" usar inc
/ dec
en la mayoría del código, especialmente cuando el resultado de la bandera no es parte de la ruta crítica.
Además de Silvermont, este es un consejo de optimización obsoleto de Pentium4 . En las CPU modernas, solo hay un problema si realmente lee una bandera que no fue escrita por el último insn que escribió las banderas. por ejemplo, en los bucles adc
BigInteger. (Y en ese caso, debe conservar CF, de modo que usar add
podría romper su código).
add
escribe todos los bits de condición-bandera en el registro EFLAGS. El cambio de nombre de registro hace que solo sea fácil de escribir para la ejecución fuera de orden: vea los riesgos de escribir después de escribir y de escribir después de leer . add eax, 1
y add ecx, 1
puede ejecutar en paralelo porque son totalmente independientes entre sí. (Incluso Pentium4 cambia el nombre de los bits de indicador de condición por separado del resto de EFLAGS, ya que incluso add
deja intactos los interrupts habilitados y muchos otros bits).
En P4, inc
y dec
dependen del valor anterior de todas las banderas , por lo que no pueden ejecutarse en paralelo entre ellas o precediendo instrucciones de ajuste de banderas. (por ejemplo, add eax, [mem]
/ inc ecx
hace que el inc
espere hasta después del add
, incluso si la carga del complemento falla en el caché.) Esto se denomina dependencia falsa . Partial-flag escribe trabajo leyendo el valor anterior de los indicadores, actualizando los bits que no sean CF, y luego escribiendo los indicadores completos.
Todas las demás CPU x86 fuera de servicio (incluidas las de AMD) cambian el nombre de las diferentes partes de las banderas por separado, por lo que internamente hacen una actualización de solo escritura para todas las banderas, excepto CF. (fuente: guía de microarquitectura de Agner Fog ). Solo unas pocas instrucciones, como adc
o cmc
, realmente leen y luego escriben banderas. Pero también shl r, cl
(ver abajo).
Casos en los que add dest, 1
es preferible a inc dest
, al menos para familias de uarch Intel P6 / SnB :
Destino de memoria :
add [rdi], 1
micro fusible de la tienda y carga + agregue Intel Core2 y familia SnB , por lo que son 2 uops de dominio fusionado u4 uops de dominio no fusionado.
inc [rdi]
solo puede microfundir la tienda, por lo que es 3F / 4U.
Según las tablas de Agner Fog, AMD y Silvermont ejecutan memory-destinc
yadd
lo mismo, como una macro-op / uop única.Pero tenga cuidado con los efectos de uop-cache con
add [label], 1
que necesita una dirección de 32 bits y una de 8 bits inmediata para el mismo uop.Antes de un cambio de cuenta variable / rotar para romper la dependencia de las banderas y evitar la fusión parcial:
shl reg, cl
tiene una dependencia de entrada en las banderas, debido a un desafortunado historial CISC: tiene que dejarlas sin modificar si el recuento de turnos es 0 .En la familia Intel SnB, los turnos de conteo variable son de 3 uops (por encima de 1 en Core2 / Nehalem). AFAICT, dos de los marcadores de lectura / escritura de uops, y un uop independiente lee
reg
ycl
, y escribereg
. Es un caso extraño de tener una mejor latencia (1c + conflictos de recursos inevitables) que el rendimiento (1.5c), y solo poder alcanzar el rendimiento máximo si se combina con instrucciones que rompen las dependencias de los indicadores. ( Publiqué más sobre esto en el foro de Agner Fog). Use BMI2shlx
cuando sea posible; es 1 uop y el conteo puede estar en cualquier registro.De todos modos,
inc
(escritura de banderas pero dejandoCF
sin modificar) antes de conteo variableshl
deja con una dependencia falsa en lo que escribió CF por última vez, y en SnB / IvB puede requerir un uop adicional para fusionar banderas.Core2 / Nehalem logran evitar incluso el falso dep on flags: Merom ejecuta un ciclo de 6 instrucciones
shl reg,cl
en casi dos turnos por reloj, el mismo rendimiento con cl = 0 o cl = 13. Cualquier cosa mejor que 1 por reloj demuestra que no hay dependencia de entrada en los indicadores.Intenté bucles con
shl edx, 2
yshl edx, 0
(cambios de conteo inmediato), pero no vi una diferencia de velocidad entredec
ysub
en Core2, HSW o SKL. No sé sobre AMD.
Actualización: El agradable rendimiento de turnos en la familia Intel P6 tiene el costo de un gran bache de rendimiento que debe evitar: cuando una instrucción depende del resultado de bandera de una instrucción de cambio: El front-end se detiene hasta que se retira la instrucción. (Fuente: manual de optimización de Intel, (Sección 3.5.2.6: Marcadores de registro parcial) ). Así que shr eax, 2
/ jnz
es bastante catastrófico para el rendimiento en Intel pre-Sandybridge, ¡supongo! Use shr eax, 2
/ test eax,eax
/ jnz
si se preocupa por Nehalem y antes. Los ejemplos de Intel dejan en claro que esto se aplica a los turnos de recuento inmediato, no solo a count = cl
.
En los procesadores basados en la microarquitectura Intel Core [esto significa Core 2 y posterior], el desplazamiento inmediato por 1 es manejado por un hardware especial, de modo que no experimenta un bloqueo parcial del indicador.
Intel realmente significa el código de operación especial sin inmediato, que se desplaza por un 1
implícito. Creo que hay una diferencia de rendimiento entre las dos formas de codificación shr eax,1
, con la codificación corta (utilizando el código de operación 8086 original D1 /5
) produciendo un resultado de bandera de solo escritura (parcial), pero la codificación más larga ( C1 /5, imm8
con un inmediato 1
) que no tiene su inmediato verificado por 0 hasta el tiempo de ejecución, pero sin rastrear la salida de bandera en la maquinaria fuera de servicio.
Como el bucle sobre bits es común, pero recorrer cada 2 o más pasos (o cualquier otro paso) es muy poco frecuente, parece una opción de diseño razonable. Esto explica por qué a los compiladores les gusta test
el resultado de un cambio en lugar de usar directamente los resultados de bandera de shr
.
Actualización: para los cambios de conteo variables en la familia SnB, el manual de optimización de Intel dice:
3.5.1.6 Rotación y cambio de cuenta de bit variable
En el nombre en código de la microarquitectura Intel Sandy Bridge, la instrucción "ROL / ROR / SHL / SHR reg, cl" tiene tres microoperaciones. Cuando no se necesita el resultado del indicador, una de estas microoperaciones puede descartarse, proporcionando un mejor rendimiento en muchos usos comunes . Cuando estas instrucciones actualizan los resultados del indicador parcial que se utilizan posteriormente, el flujo total de tres microoperaciones debe pasar por la canalización de ejecución y retiro, experimentando un rendimiento más lento. En el nombre de código de microarquitectura Intel Ivy Bridge, la ejecución del flujo completo de tres microoperaciones para utilizar el resultado de indicador parcial actualizado tiene un retraso adicional.
Considera la siguiente secuencia en bucle:
loop: shl eax, cl add ebx, eax dec edx ; DEC does not update carry, causing SHL to execute slower three micro-ops flow jnz loop
La instrucción DEC no modifica la bandera de acarreo. En consecuencia, la instrucción SHL EAX, CL necesita ejecutar el flujo de tres microoperaciones en iteraciones posteriores. La instrucción SUB actualizará todos los indicadores. Entonces, reemplazar
DEC
conSUB
permitirá aSHL EAX, CL
ejecutar el flujo de las dos microoperaciones.
Terminología
Los puestos de bandera parcial suceden cuando se leen banderas , si es que ocurren. P4 nunca tiene puestos de bandera parcial, porque nunca necesitan fusionarse. Tiene dependencias falsas en su lugar.
Varias respuestas / comentarios mezclan la terminología. Describen una dependencia falsa, pero luego la llaman un puesto de bandera parcial. Es una ralentización que ocurre debido a que solo se escriben algunos de los indicadores, pero el término " pérdida de bandera parcial" es lo que sucede en el hardware Intel pre-SnB cuando las escrituras de bandera parcial deben fusionarse. Las CPU Intel SnB-family insertan un uop extra para unir banderas sin atascos. Nehalem y el puesto anterior durante ~ 7 ciclos. No estoy seguro de cuán grande es la penalización para las CPU AMD.
(Tenga en cuenta que las penalizaciones de registro parcial no son siempre las mismas que las de parcial, ver más abajo).
### Partial flag stall on Intel P6-family CPUs:
bigint_loop:
adc eax, [array_end + rcx*4] # partial-flag stall when adc reads CF
inc rcx # rcx counts up from negative values towards zero
# test rcx,rcx # eliminate partial-flag stalls by writing all flags, or better use add rcx,1
jnz
# this loop doesn''t do anything useful; it''s not normally useful to loop the carry-out back to the carry-in for the same accumulator.
# Note that `test` will change the input to the next adc, and so would replacing inc with add 1
En otros casos, por ejemplo, una escritura de bandera parcial seguida de una escritura de bandera completa, o una lectura de solo banderas escritas por inc
, está bien. En las CPU de la familia SnB, inc/dec
incluso puede fusionarse en macro con un jcc
, al igual que add/sub
.
Después de P4, Intel casi se dio por vencido al tratar de hacer que la gente vuelva a compilar con -mtune=pentium4
o modifique el asm escrito a mano para evitar serios cuellos de botella. (El ajuste para una microarquitectura específica siempre será una cosa, pero P4 era inusual en desaprobar tantas cosas que solían ser rápidas en CPUs anteriores , y por lo tanto eran comunes en los binarios existentes). P4 quería que las personas usaran un subconjunto similar a RISC de el x86, y también tenía sugerencias de predicción de bifurcación como prefijos para las instrucciones de JCC. (También tenía otros problemas graves, como la caché de rastreo que simplemente no era lo suficientemente buena, y decodificadores débiles que significaban un mal rendimiento en errores de caché de rastreo. Sin mencionar que toda la filosofía de sincronización llegó muy alto al muro de densidad de potencia .)
Cuando Intel abandonó P4 (netburst uarch), volvieron a los diseños de la familia P6 (Pentium-M / Core2 / Nehalem) que heredaron su manejo de bandera parcial / parcial de CPUs anteriores de la familia P6 (PPro a PIII) que pre fechado el mal paso de Netburst. (No todo lo relacionado con P4 era intrínsecamente malo, y algunas de las ideas reaparecieron en Sandybridge, pero en general NetBurst se considera un error). Algunas instrucciones muy CISC son aún más lentas que las alternativas de instrucción múltiple, por ejemplo, enter
, loop
, o bt [mem], reg
(porque el valor de reg afecta a qué dirección de memoria se usa), pero todo esto era lento en las CPU más antiguas, por lo que los compiladores ya las habían evitado.
Pentium-M incluso mejoró el soporte de hardware para parciales (menores penalizaciones de fusión). En Sandybridge, Intel mantuvo el cambio de nombre parcial y de registro parcial y lo hizo mucho más eficiente cuando se necesita la fusión (fusionar uop insertado con no o mínimo puesto). SnB realizó importantes cambios internos y se lo considera una nueva familia de uarch, a pesar de que hereda mucho de Nehalem, y algunas ideas de P4. (Pero tenga en cuenta que el caché descodificado-uop de SnB no es un caché de rastreo, por lo que es una solución muy diferente al problema de rendimiento / potencia del decodificador que el caché de rastreo de Netburst intentó resolver).
Por ejemplo, inc al
y inc ah
pueden funcionar en paralelo en las CPU de la familia P6 / SnB, pero leer eax
luego requiere fusión .
Pérdida de PPro / PIII durante 5-6 ciclos al leer el registro completo. Core2 / Nehalem se detienen solo durante 2 o 3 ciclos al insertar un uop de fusión para regs parciales, pero los indicadores parciales siguen siendo más largos.
SnB inserta un uop de fusión sin estancamiento, como para banderas. La guía de optimización de Intel dice que para fusionar AH / BH / CH / DH en el registro más amplio, insertar el uop de fusión requiere un ciclo completo de problema / cambio de nombre durante el cual no se pueden asignar otros uops. Pero para low8 / low16, el uop de fusión es "parte del flujo", por lo que aparentemente no causa penalizaciones de rendimiento adicionales en el extremo frontal más allá de ocupar uno de los 4 espacios en un ciclo de problema / cambio de nombre.
En IvyBridge (o al menos Haswell), Intel descartó el registro parcial de los registros low8 y low16, manteniéndolo solo para los registros high8 (AH / BH / CH / DH). La lectura de registros high8 tiene latencia adicional. Además, setcc al
tiene una dependencia falsa del antiguo valor de rax, a diferencia de Nehalem y anteriores (y probablemente Sandybridge). Consulte este Q & A de rendimiento de registro parcial de HSW / SKL para obtener más detalles.
(Anteriormente afirmé que Haswell podía fusionar AH sin UOP, pero eso no es cierto ni tampoco lo que dice la guía de Agner Fog. Rastreé demasiado rápido y desafortunadamente repetí mi entendimiento equivocado en muchos comentarios y otras publicaciones).
Las CPU de AMD e Intel Silvermont no cambian el nombre de los regs parciales (que no sean indicadores), de modo que mov al, [mem]
tiene una dependencia falsa del antiguo valor de eax. (El lado positivo no es la desaceleración de la fusión parcial cuando se lee el registro completo más adelante).
Normalmente, la única vez que add
lugar de inc
hará que su código sea más rápido en AMD o en la corriente principal de Intel, es cuando su código realmente depende del comportamiento de inc
de no tocar CF. es decir, normalmente add
solo ayuda cuando rompería tu código , pero shl
caso shl
mencionado anteriormente, donde la instrucción lee banderas, pero por lo general a tu código no le importa eso, así que es una dependencia falsa.
Si realmente quiere dejar CF sin modificar, las CPU anteriores de la familia SnB tienen serios problemas con las paradas de bandera parcial, pero en la familia SnB la sobrecarga de tener la CPU fusionando las banderas parciales es muy baja, por lo que puede ser mejor mantener usar inc
o dec
como parte de una condición de bucle cuando se dirige a esas CPU, con algo de desenrollamiento. (Para más detalles, consulte la sección de Preguntas y adc
BigInteger que he vinculado anteriormente). Puede ser útil usar lea
para hacer aritmética sin afectar banderas en absoluto, si no necesita ramificar el resultado.