Cómo cambiar una sola letra en el ensamblador acelera la división tres veces: Un desglose técnico
Cambiar un solo carácter en una instrucción de ensamblado reduce el tiempo de la operación de división en un 66 %. Benchmarks del mundo real en CPUs modernas muestran cómo las dependencias falsas de registros convierten el potencial de ejecución desordenada en un cuello de botella. Desglosamos las sutilezas arquitectónicas de x86-64 que afectan el rendimiento.
Características arquitectónicas de los registros x86-64
Los procesadores x86-64 modernos usan una jerarquía de registros con una estructura heredada. El registro rax (64 bits) incluye:
┌───────────────────────────────────────────────────────────────┐
│ rax (64 bits) │
├───────────────────────────────┬───────────────────────────────┤
│ (32 superiores) │ eax (32 bits) │
│ ├───────────────┬───────────────┤
│ │ │ ax (16 bits)│
│ │ ├───────┬───────┤
│ │ │ ah (8)│ al (8)│
└───────────────────────────────┴───────────────┴───────┴───────┘
Punto clave: Escribir en las partes inferiores del registro (ax, al) no afecta los bits superiores, mientras que las operaciones con registros de 32 bits (eax) sobrescriben completamente los 32 bits superiores. Esto crea dependencias ocultas críticas para el pipelining.
Las instrucciones de división div/idiv usan un par de registros:
┌──────────┬────────────────┬──────────────┬─────────────────────┐
│ Tamaño │ Dividendo │ Cociente en │ Resto en │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 16 bits │ dx:ax │ ax │ dx │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 32 bits │ edx:eax │ eax │ edx │
├──────────┼────────────────┼──────────────┼─────────────────────┤
│ 64 bits │ rdx:rax │ rax │ rdx │
└──────────┴────────────────┴──────────────┴─────────────────────┘
Antes de la división, la parte superior del par debe zerarse. Patrón estándar:
mov edx, 0
mov eax, 536700
div dword [denominator]
Datos experimentales: Diferencia de 3.7×
El bucle de prueba procesó 2.073.600 iteraciones (1920×1080), simulando renderizado de píxeles. Resultados en ciclos por iteración (después de restar el overhead del bucle):
┌────────┬──────────────┬──────────────┐
│ │ idiv (limpio)│ div (limpio) │
├────────┼──────────────┼──────────────┤
│ 16 bits│ 22.2 │ 22.2 │
├────────┼──────────────┼──────────────┤
│ 32 bits│ 6.0 │ 6.0 │
├────────┼──────────────┼──────────────┤
│ 64 bits│ 26.2 │ 24.1 │
└────────┴──────────────┴──────────────┘
Sorpresa: las operaciones de 32 bits se ejecutan 3,7 veces más rápido que las de 16 bits con datos de entrada idénticos. La razón no es la complejidad del algoritmo de división, sino detalles de ejecución microarquitectónicos.
Mecanismo de las dependencias falsas
Los procesadores usan la Tabla de Alias de Registros (RAT) para mapear registros lógicos a físicos. Notablemente:
- Escribir en un registro de 32 bits (
mov edx, ...) sobrescribe completamente el valor, rompiendo dependencias con operaciones previas - Escribir en un registro de 16 bits (
mov dx, ...) requiere mezclar los nuevos datos con los bits superiores, creando una dependencia falsa
En la variante de 16 bits, la cadena de dependencias se ve así:
div word → merge → mov dx → merge → div word → ...
Cada iteración debe esperar a la anterior debido a las necesidades de fusión de registros. En la variante de 32 bits, esta cadena se rompe, permitiendo al procesador pipelining la ejecución.
Métricas clave de rendimiento
- Latencia: 21–22 ciclos para
divde 16 bits (tiempo hasta que el resultado está listo) - Throughput: 6 ciclos para
divde 16 bits (intervalo de despacho entre operaciones)
Cuando las operaciones dependen unas de otras, la latencia es el cuello de botella. Para operaciones independientes, el throughput. La relación 22/6 ≈ 3,7 explica la aceleración.
Verificación de la hipótesis: Reemplazando una letra
El experimento confirmó la teoría. Reemplazando instrucciones de 16 bits con de 32 bits en un contexto de 16 bits:
; Antes
mov dx, 0x0008
mov ax, 0x2B7C
; Después
mov edx, 8
mov eax, 0x2B7C
Llevó a resultados dramáticos:
┌───────────────────────┬──────────────┬──────────────┐
│ │ idiv (limpio)│ div (limpio) │
├───────────────────────┼──────────────┼──────────────┤
│ 16 bits (mov dx) │ 22.2 │ 22.2 │
├───────────────────────┼──────────────┼──────────────┤
│ 16 bits (mov edx) │ 7.0 │ 7.0 │
└───────────────────────┴──────────────┴──────────────┘
Una sola letra e en la instrucción eliminó la dependencia falsa, reduciendo el tiempo de ejecución de 22,2 a 7,0 ciclos: directamente al nivel de throughput del divisor.
Lo más importante
- La elección del tamaño del registro es crítica: Usar operaciones de 32 bits para preparar una división de 16 bits elimina dependencias innecesarias
- Latencia frente a throughput: Las dependencias hacen que el rendimiento dependa de la latencia, no del throughput
- Sutilezas arquitectónicas: Entender la RAT y la fusión parcial de registros es esencial para la optimización de bajo nivel
- El divisor produce el resto más rápido: x86 calcula cociente y resto en paralelo, permitiendo un uso más temprano de edx respecto a rax
- La prueba es obligatoria: Las estimaciones teóricas a menudo divergen de las mediciones reales debido a peculiaridades microarquitectónicas
Recomendaciones prácticas para desarrolladores
- Para la división, siempre usa operaciones de 32 bits para zerar las partes superiores de los registros, incluso con datos de 16 bits
- Evita mezclar tamaños de registros en bucles
- Para código crítico en rendimiento, revisa la salida de ensamblado del compilador
- Nota que el throughput del divisor supera la latencia, pero solo sin dependencias
- En CPUs modernas (Ice Lake en adelante), consulta las especificaciones de uops.info para métricas actuales
Este experimento muestra cómo un conocimiento profundo de la microarquitectura descubre optimizaciones no obvias. Cambiar una letra aparentemente insignificante en una instrucción produce una aceleración triple al eliminar límites artificiales del pipeline. Para programadores de sistemas, es un recordatorio: incluso en ensamblado de bajo nivel, las oportunidades de optimización se ocultan en la comprensión del hardware.
— Editorial Team
Aún no hay comentarios.