x64 tutorial tipos tiempo tengo sistemas significa segundo resueltos rendimiento reloj registros qué que programa procesador posee por operativos modos mismo lenguaje instrucción instrucciones instruccion funciones formato español entre ensamblador ejercicios ejemplos ejecuta direccionamiento diferencia desplazamiento definicion defina datos curso constantes con computadoras computadora como cisc ciclos carpeta calculo calcular brevemente basicas assembler arquitectura archivos aplicaciones performance assembly optimization x86 micro-optimization

performance - tutorial - tipos de datos en arquitectura de computadoras



¿Qué métodos se pueden usar para extender de manera eficiente la longitud de la instrucción en el x86 moderno? (4)

Imagina que quieres alinear una serie de instrucciones de ensamblaje x86 con ciertos límites. Por ejemplo, es posible que desee alinear los bucles con un límite de 16 o 32 bytes, o empaquetar las instrucciones para que se coloquen de manera eficiente en el caché uop o lo que sea.

La forma más sencilla de lograr esto es mediante instrucciones NOP de un solo byte, seguidas de cerca por los NOP de múltiples bytes . Aunque este último es generalmente más eficiente, ninguno de los métodos es gratuito: los NOP utilizan recursos de ejecución de front-end, y también cuentan en contra de su límite de 4 nombres de 1 ancho en x86 moderno.

Otra opción es alargar de alguna manera algunas instrucciones para obtener la alineación que desea. Si esto se hace sin introducir nuevos puestos, parece mejor que el enfoque NOP. ¿Cómo se pueden alargar las instrucciones de manera eficiente en las recientes CPU x86?

En el mundo ideal las técnicas de alargamiento serían simultáneamente:

  • Aplicable a la mayoría de las instrucciones.
  • Capaz de alargar la instrucción por una cantidad variable.
  • No detiene ni ralentiza los decodificadores.
  • Ser representado eficientemente en la caché uop

No es probable que exista un solo método que satisfaga todos los puntos anteriores de manera simultánea, por lo que las buenas respuestas probablemente abordarán varios compromisos.

1 El límite es 5 o 6 en AMD Ryzen.


Depende de la naturaleza del código.

Código pesado punto flotante

Prefijo AVX

Se puede recurrir al prefijo AVX más largo para la mayoría de las instrucciones de SSE. Tenga en cuenta que hay una penalización fija cuando se cambia entre SSE y AVX en las CPU intel [1] [2] . Esto requiere vzeroupper, que puede interpretarse como otro NOP para el código SSE o el código AVX que no requiere los 128 bits más altos.

SSE / AVX NOPS

Los NOP típicos que se me ocurren son:

  • XORPS el mismo registro, use variaciones SSE / AVX para enteros de estos
  • ANDPS el mismo registro, use variaciones SSE / AVX para los enteros de estos

Puedo pensar en cuatro formas fuera de mi cabeza:

Primero: Use codificaciones alternativas para las instrucciones (Peter Cordes mencionó algo similar). Hay muchas formas de llamar a la operación ADD, por ejemplo, y algunas de ellas ocupan más bytes:

http://www.felixcloutier.com/x86/ADD.html

Por lo general, un ensamblador intentará elegir la "mejor" codificación para la situación, ya sea que esté optimizando la velocidad o la longitud, pero siempre puede usar otra y obtener el mismo resultado.

Segundo: Use otras instrucciones que signifiquen lo mismo y tengan diferentes longitudes. Estoy seguro de que puede pensar en innumerables ejemplos en los que podría colocar una instrucción en el código para reemplazar una existente y obtener los mismos resultados. Las personas que optimizan el código de la mano lo hacen todo el tiempo:

shl 1 add eax, eax mul 2 etc etc

Tercero: use la variedad de NOP disponibles para eliminar espacio adicional:

nop and eax, eax sub eax, 0 etc etc

En un mundo ideal, es probable que tengas que usar todos estos trucos para que el código tenga la longitud exacta de bytes que deseas.

Cuarto: cambie su algoritmo para obtener más opciones usando los métodos anteriores.

Una nota final: Obviamente, apuntar a procesadores más modernos le dará mejores resultados debido a la cantidad y complejidad de las instrucciones. Tener acceso a las instrucciones MMX, XMM, SSE, SSE2, punto flotante, etc. podría facilitar su trabajo.


Veamos una pieza específica de código:

cmp ebx,123456 mov al,0xFF je .foo

Para este código, ninguna de las instrucciones se puede reemplazar con ninguna otra cosa, por lo que las únicas opciones son prefijos y NOP redundantes.

Sin embargo, ¿qué pasa si cambias el orden de las instrucciones?

Podrías convertir el código en esto:

mov al,0xFF cmp ebx,123456 je .foo

Después de reordenar las instrucciones; El mov al,0xFF podría reemplazarse con or eax,0x000000FF or ax,0x00FF .

Para la primera ordenación de instrucciones solo hay una posibilidad, y para la segunda ordenación de instrucciones hay 3 posibilidades; así que hay un total de 4 posibles permutaciones para elegir sin usar ningún prefijo redundante o NOP.

Para cada una de esas 4 permutaciones puede agregar variaciones con diferentes cantidades de prefijos redundantes, y NOP de un solo byte y multi-byte, para hacer que termine en una alineación específica / s. Soy demasiado perezoso para hacer las matemáticas, así que supongamos que tal vez se expanda a 100 posibles permutaciones.

¿Qué pasaría si le dieras una puntuación a cada una de estas 100 permutaciones? (En función de cuánto tiempo llevaría ejecutarse, qué tan bien alinea la instrucción después de esta pieza, si el tamaño o la velocidad son importantes, ...). Esto puede incluir la segmentación micro-arquitectónica (por ejemplo, tal vez para algunas CPU la permutación original rompe la fusión de micro-op y empeore el código).

Podría generar todas las permutaciones posibles y darles una puntuación, y elegir la permutación con la mejor puntuación. Tenga en cuenta que esto puede no ser la permutación con la mejor alineación (si la alineación es menos importante que otros factores y solo empeora el rendimiento).

Por supuesto, puede dividir los programas grandes en muchos grupos pequeños de instrucciones lineales separadas por cambios de flujo de control; y luego haga esta "búsqueda exhaustiva de la permutación con la mejor puntuación" para cada grupo pequeño de instrucciones lineales.

El problema es que el orden de instrucción y la selección de instrucción son co-dependientes.

Para el ejemplo anterior, no se pudo reemplazar mov al,0xFF hasta después de que reordenemos las instrucciones; y es fácil encontrar casos en los que no puede volver a ordenar las instrucciones hasta que haya reemplazado (algunas) instrucciones. Esto dificulta la búsqueda exhaustiva de la mejor solución, de cualquier definición de "mejor", incluso si solo le interesa la alineación y no le importa el rendimiento en absoluto.


Considere el uso de código suave para reducir su código en lugar de expandirlo , especialmente antes de un ciclo. por ejemplo, xor eax,eax / cdq si necesita dos registros lea ecx, [rax+1] cero, o mov eax, 1 / lea ecx, [rax+1] para establecer registros en 1 y 2 en solo 8 bytes en lugar de 10. Vea Establecer todos los bits en la CPU regístrese a 1 de manera eficiente para obtener más información al respecto, y Consejos para jugar al golf en el código de máquina x86 / x64 para obtener ideas más generales. Probablemente todavía quieras evitar las dependencias falsas, sin embargo.

O llene espacio adicional creando una constante vectorial sobre la marcha en lugar de cargarla de la memoria. (Sin embargo, agregar más presión de caché uop podría ser peor para el bucle más grande que contiene su configuración + bucle interno. Pero evita las constantes, por lo que tiene una ventaja para compensar la ejecución de más uops).

Si aún no los estaba utilizando para cargar constantes "comprimidas", pmovsxbd , movddup o vpbroadcastd son más largos que movaps . Las cargas de difusión de dword / qword son gratuitas (no ALU uop, solo una carga).

Si está preocupado por la alineación del código, probablemente le preocupe cómo se ubica en el caché L1I o dónde se encuentran los límites del caché uop, por lo que no basta con contar el total de uops, y algunos uops adicionales en el Bloquear antes de la persona que te importa puede no ser un problema en absoluto.

Pero en algunas situaciones, es posible que desee optimizar el rendimiento de decodificación / uop-cache use / total uops para las instrucciones antes del bloque que desea alinear.

Instrucciones de relleno, como la pregunta formulada:

Agner Fog tiene una sección completa sobre esto: "10.6 Hacer que las instrucciones sean más largas en aras de la alineación" en su guía "Optimización de subrutinas en lenguaje ensamblador" . (Las ideas de lea , push r/m64 y SIB son de allí, y copié una oración / frase o dos, de lo contrario esta respuesta es mi propio trabajo, ya sea ideas diferentes o escritas antes de consultar la guía de Agner).

Sin embargo, no se ha actualizado para las CPU actuales: lea eax, [rbx + dword 0] tiene más desventajas que las que usó para vs mov eax, ebx , porque se pierde la latencia cero / no hay unidad de ejecución mov . Si no está en el camino crítico, adelante. Simple lea tiene un rendimiento bastante bueno, y un LEA con un modo de direccionamiento grande (y quizás incluso algunos prefijos de segmento) puede ser mejor para el rendimiento de decodificación / ejecución que mov + nop .

Use la forma general en lugar de la forma corta (sin ModR / M) de instrucciones como push reg o mov reg,imm . Por ejemplo, use 2-byte push r/m64 para push rbx . O utilice una instrucción equivalente que sea más larga, como add dst, 1 lugar de inc dst , en los casos en que no haya desventajas de perf para inc por lo que ya estaba usando inc .

Utilizar byte SIB . Puede hacer que NASM haga eso usando un solo registro como índice, como mov eax, [nosplit rbx*1] ( vea también ), pero eso afecta la latencia de uso de la carga en lugar de simplemente codificar mov eax, [rbx] con un byte SIB. Los modos de direccionamiento indexado tienen otras desventajas en la familia SnB, como la no laminación y no usar port7 para las tiendas .

Por lo tanto , es mejor codificar base=rbx + disp0/8/32=0 utilizando ModR / M + SIB sin registro de índice . (La codificación SIB para "no índice" es la codificación que de otra manera significaría idx = RSP). [rsp + x] modos de direccionamiento [rsp + x] ya requieren un SIB (base = RSP es el código de escape que significa que hay un SIB), y aparece todo el tiempo en el código generado por el compilador. Así que hay una buena razón para esperar que esto sea completamente eficiente para decodificar y ejecutar (incluso para registros base distintos de RSP) ahora y en el futuro. La sintaxis NASM no puede expresar esto, por lo que tendría que codificar manualmente. La sintaxis Intel de gas GNU de objdump -d dice 8b 04 23 mov eax,DWORD PTR [rbx+riz*1] para el ejemplo 10.20 de Agner Fog. ( riz es una notación ficticia de índice cero que significa que hay un SIB sin índice). No he probado si GAS acepta eso como entrada.

Use una forma imm32 y / o disp32 de una instrucción que solo necesite imm8 o disp0/disp32 . La prueba de Agner Fog de la caché uop de Sandybridge ( tabla 9.1 de la guía de microarquía ) indica que el valor real de un desplazamiento inmediato es lo que importa, no el número de bytes utilizados en la codificación de la instrucción. No tengo ninguna información sobre el caché uop de Ryzen.

Así que NASM imul eax, [dword 4 + rdi], strict dword 13 (10 bytes: opcode + modrm + disp32 + imm32) utilizaría la categoría 32small, 32small y tomaría 1 entrada en el caché uop, a diferencia de si es inmediato o disp32 En realidad tenía más de 16 bits significativos. (Luego tomaría 2 entradas, y cargarlo desde la caché uop tomaría un ciclo adicional).

Según la tabla de Agner, 8/16 / 32small siempre son equivalentes para SnB. Y los modos de direccionamiento con un registro son los mismos ya sea que no haya ningún desplazamiento, o si es 32small, por lo que mov dword [dword 0 + rdi], 123456 toma 2 entradas, como mov dword [rdi], 123456789 . No me había dado cuenta de que [rdi] + full imm32 tomó 2 entradas, pero aparentemente ese es el caso de SnB.

Use jmp / jcc rel32 lugar de rel8 . Lo ideal es tratar de expandir las instrucciones en lugares que no requieran codificaciones de salto más largas fuera de la región que está expandiendo. Pad después de los objetivos de salto para los saltos hacia adelante anteriores, el pad antes de los objetivos de salto para saltos hacia atrás posteriores, si están cerca de necesitar un rel32 en otro lugar. es decir, intente evitar el relleno entre una rama y su destino, a menos que quiera que esa rama utilice un rel32 de todos modos.

Es posible que tenga la tentación de codificar mov eax, [symbol] como a32 mov eax, [abs symbol] 6 bytes a32 mov eax, [abs symbol] en un código de 64 bits, usando un prefijo de tamaño de dirección para usar una dirección absoluta de 32 bits. Pero esto causa un bloqueo del prefijo que cambia de longitud cuando se decodifica en las CPU de Intel. Afortunadamente, ninguno de NASM / YASM / gas / clang realiza esta optimización de tamaño de código por defecto si no especifica explícitamente un tamaño de dirección de 32 bits, en lugar de eso utiliza 7-byte mov r32, r/m32 con un ModR / M + SIB + disp32 modo de direccionamiento absoluto para mov eax, [abs symbol] .

En el código dependiente de la posición de 64 bits, el direccionamiento absoluto es una forma económica de usar 1 byte extra en comparación con el RIP relativo . Pero tenga en cuenta que lo absoluto + inmediato de 32 bits requiere 2 ciclos para obtener de la caché uop, a diferencia de RIP-relative + imm8 / 16/32, que toma solo 1 ciclo aunque todavía usa 2 entradas para la instrucción. (por ejemplo, para una tienda mov o un cmp ). Por lo tanto, cmp [abs symbol], 123 es más lento de obtener de la caché uop que cmp [rel symbol], 123 , aunque ambos toman 2 entradas cada uno. Sin una inmediata, no hay costo extra para

Tenga en cuenta que los ejecutables PIE permiten ASLR incluso para el ejecutable, y son los valores predeterminados en muchas distribuciones de Linux , por lo que si puede mantener su PIC de código sin ningún inconveniente, entonces es preferible.

Use un prefijo REX cuando no lo necesite, por ejemplo, db 0x40 / add eax, ecx .

En general, no es seguro agregar prefijos como la representación que ignoran las CPU actuales, ya que pueden significar algo más en las futuras extensiones ISA.

A veces es posible repetir el mismo prefijo (aunque no con REX). Por ejemplo, db 0x66, 0x66 / add ax, bx proporciona los prefijos de tamaño de operando de la instrucción 3, que creo que siempre es estrictamente equivalente a una copia del prefijo. Hasta 3 prefijos es el límite para una decodificación eficiente en algunas CPU. Pero esto solo funciona si tienes un prefijo que puedes usar en primer lugar; por lo general, no está utilizando un tamaño de operando de 16 bits y, en general, no desea un tamaño de dirección de 32 bits (aunque es seguro para acceder a datos estáticos en un código que depende de la posición).

Un prefijo ds o ss en una instrucción que accede a la memoria es un no-op , y probablemente no cause ninguna desaceleración en ninguna CPU actual. (@prl sugirió esto en los comentarios).

De hecho, la guía de microarquía de Agner Fog utiliza un prefijo ds en un movq [esi+ecx],mm0 en el Ejemplo 7.1. Organizar los bloques IFETCH para sintonizar un bucle para PII / PIII (sin búfer de bucle o caché uop), acelerándolo de 3 iteraciones por reloj a 2.

Algunas CPU (como AMD) se decodifican lentamente cuando las instrucciones tienen más de 3 prefijos. En algunas CPU, esto incluye los prefijos obligatorios en SSE2 y especialmente las instrucciones SSSE3 / SSE4.1. En Silvermont, incluso el byte de escape 0F cuenta.

Las instrucciones de AVX pueden usar un prefijo VEX de 2 o 3 bytes . Algunas instrucciones requieren un prefijo VEX de 3 bytes (la segunda fuente es x / ymm8-15, o prefijos obligatorios para SSSE3 o posterior). Pero una instrucción que podría haber usado un prefijo de 2 bytes siempre puede codificarse con un VEX de 3 bytes. NASM o GAS {vex3} vxorps xmm0,xmm0 . Si AVX512 está disponible, también puede usar EVEX de 4 bytes.

Use el tamaño de operando de 64 bits para mov incluso cuando no lo necesite , por ejemplo mov rax, strict dword 1 fuerza la codificación de 7 bytes con signo de extensión 32 en NASM, que normalmente lo optimizaría a mov eax, 1 5 bytes mov eax, 1 .

mov eax, 1 ; 5 bytes to encode (B8 imm32) mov rax, strict dword 1 ; 7 bytes: REX mov r/m64, sign-extended-imm32. mov rax, strict qword 1 ; 10 bytes to encode (REX B8 imm64). movabs mnemonic for AT&T.

Incluso podrías usar mov reg, 0 lugar de xor reg,reg .

mov r64, imm64 encaja eficientemente en el caché uop cuando la constante es realmente pequeña (se ajusta en el signo de 32 bits extendido). 1 entrada de caché uop, y tiempo de carga = 1, igual que para mov r32, imm32 . Decodificar una instrucción gigante significa que probablemente no haya espacio en un bloque de decodificación de 16 bytes para que otras 3 instrucciones se decodifiquen en el mismo ciclo, a menos que sean todas de 2 bytes. Posiblemente alargar un poco otras instrucciones puede ser mejor que tener una instrucción larga.

Decodificar penalizaciones por prefijos adicionales:

  • P5: los prefijos impiden el emparejamiento, a excepción de la dirección / tamaño de operando solo en PMMX.
  • PPro a PIII: siempre hay una penalización si una instrucción tiene más de un prefijo. Esta penalización suele ser de un reloj por prefijo adicional. (Guía de microarquía de Agner, final de la sección 6.3)
  • Silvermont: probablemente sea la restricción más estricta sobre qué prefijos puede usar, si le interesa. Decodifique las paradas en más de 3 prefijos, contando los prefijos obligatorios + 0F byte de escape. Las instrucciones SSSE3 y SSE4 ya tienen 3 prefijos, por lo que incluso un REX hace que se decodifiquen lentamente.
  • algunos AMD: tal vez un límite de 3 prefijos, sin incluir los bytes de escape, y tal vez sin incluir los prefijos obligatorios para las instrucciones SSE.

... TODO: termina esta sección. Hasta entonces, consulte la guía de microarcas de Agner Fog.

Después de codificar a mano, desmonte siempre su binario para asegurarse de que lo hizo bien . Es lamentable que NASM y otros ensambladores no tengan un mejor soporte para elegir el relleno barato en una región de instrucciones para alcanzar un límite de alineación determinado.

Sintaxis del ensamblador

NASM tiene alguna sintaxis de anulación de codificación : los {vex3} y {evex} , NOSPLIT y strict byte / dword , y forzando disp8 / disp32 dentro de los modos de direccionamiento. Tenga en cuenta que [rdi + byte 0] no está permitido, la palabra clave de byte debe aparecer primero. Se permite [byte rdi + 0] , pero creo que se ve raro.

Listado de nasm -l/dev/stdout -felf64 padding.asm

line addr machine-code bytes source line num 4 00000000 0F57C0 xorps xmm0,xmm0 ; SSE1 *ps instructions are 1-byte shorter 5 00000003 660FEFC0 pxor xmm0,xmm0 6 7 00000007 C5F058DA vaddps xmm3, xmm1,xmm2 8 0000000B C4E17058DA {vex3} vaddps xmm3, xmm1,xmm2 9 00000010 62F1740858DA {evex} vaddps xmm3, xmm1,xmm2 10 11 12 00000016 FFC0 inc eax 13 00000018 83C001 add eax, 1 14 0000001B 4883C001 add rax, 1 15 0000001F 678D4001 lea eax, [eax+1] ; runs on fewer ports and doesn''t set flags 16 00000023 67488D4001 lea rax, [eax+1] ; address-size and REX.W 17 00000028 0501000000 add eax, strict dword 1 ; using the EAX-only encoding with no ModR/M 18 0000002D 81C001000000 db 0x81, 0xC0, 1,0,0,0 ; add eax,0x1 using the ModR/M imm32 encoding 19 00000033 81C101000000 add ecx, strict dword 1 ; non-eax must use the ModR/M encoding 20 00000039 4881C101000000 add rcx, strict qword 1 ; YASM requires strict dword for the immediate, because it''s still 32b 21 00000040 67488D8001000000 lea rax, [dword eax+1] 22 23 24 00000048 8B07 mov eax, [rdi] 25 0000004A 8B4700 mov eax, [byte 0 + rdi] 26 0000004D 3E8B4700 mov eax, [ds: byte 0 + rdi] 26 ****************** warning: ds segment base generated, but will be ignored in 64-bit mode 27 00000051 8B8700000000 mov eax, [dword 0 + rdi] 28 00000057 8B043D00000000 mov eax, [NOSPLIT dword 0 + rdi*1] ; 1c extra latency on SnB-family for non-simple addressing mode

GAS tiene pseudo-prefijos de anulación de codificación {vex3} , {evex} , {disp8} y {disp32} Estos reemplazan los sufijos .s , .d8 y .d32 , ahora en desuso .

GAS no tiene una anulación al tamaño inmediato, solo desplazamientos.

GAS le permite agregar un prefijo ds explícito, con ds mov src,dst

gcc -g -c padding.S && objdump -drwC padding.o -S , con edición manual:

# no CPUs have separate ps vs. pd domains, so there''s no penalty for mixing ps and pd loads/shuffles 0: 0f 28 07 movaps (%rdi),%xmm0 3: 66 0f 28 07 movapd (%rdi),%xmm0 7: 0f 58 c8 addps %xmm0,%xmm1 # not equivalent for SSE/AVX transitions, but sometimes safe to mix with AVX-128 a: c5 e8 58 d9 vaddps %xmm1,%xmm2, %xmm3 # default {vex2} e: c4 e1 68 58 d9 {vex3} vaddps %xmm1,%xmm2, %xmm3 13: 62 f1 6c 08 58 d9 {evex} vaddps %xmm1,%xmm2, %xmm3 19: ff c0 inc %eax 1b: 83 c0 01 add $0x1,%eax 1e: 48 83 c0 01 add $0x1,%rax 22: 67 8d 40 01 lea 1(%eax), %eax # runs on fewer ports and doesn''t set flags 26: 67 48 8d 40 01 lea 1(%eax), %rax # address-size and REX # no equivalent for add eax, strict dword 1 # no-ModR/M .byte 0x81, 0xC0; .long 1 # add eax,0x1 using the ModR/M imm32 encoding 2b: 81 c0 01 00 00 00 add $0x1,%eax # manually encoded 31: 81 c1 d2 04 00 00 add $0x4d2,%ecx # large immediate, can''t get GAS to encode this way with $1 other than doing it manually 37: 67 8d 80 01 00 00 00 {disp32} lea 1(%eax), %eax 3e: 67 48 8d 80 01 00 00 00 {disp32} lea 1(%eax), %rax mov 0(%rdi), %eax # the 0 optimizes away 46: 8b 07 mov (%rdi),%eax {disp8} mov (%rdi), %eax # adds a disp8 even if you omit the 0 48: 8b 47 00 mov 0x0(%rdi),%eax {disp8} ds mov (%rdi), %eax # with a DS prefix 4b: 3e 8b 47 00 mov %ds:0x0(%rdi),%eax {disp32} mov (%rdi), %eax 4f: 8b 87 00 00 00 00 mov 0x0(%rdi),%eax {disp32} mov 0(,%rdi,1), %eax # 1c extra latency on SnB-family for non-simple addressing mode 55: 8b 04 3d 00 00 00 00 mov 0x0(,%rdi,1),%eax

GAS es estrictamente menos poderoso que NASM para expresar codificaciones más largas de lo necesario.