La operación de división es una de las instrucciones más costosas en términos de ciclos de CPU. Por esta razón, los compiladores modernos de C++ rara vez utilizan la instrucción IDIV cuando el divisor es una constante conocida en tiempo de compilación. En su lugar, aplican diversas técnicas algebraicas para transformar la división en una combinación de multiplicaciones y desplazamientos de bits (shifts), mucho más eficientes.
1. División por potencias de dos
Cuando el divisor es una potencia de dos (como 2, 4, 8, etc.), el compilador utiliza el desplazamiento aritmético a la derecha (SAR). Sin embargo, dado que la división entera en C++ debe truncar hacia cero, se requiere un ajuste adicional cuando el dividendo es negativo.
// Ejemplo en C++
int dividirPorCuatro(int valor) {
return valor / 4;
}
// Representación lógica en desensamblado (x86)
mov eax, [ebp+arg_0] ; Cargar el dividendo
cdq ; Extender el signo de EAX a EDX
and edx, 3 ; Si es negativo, EDX = 3 (2^n - 1), si no, 0
add eax, edx ; Ajuste para redondeo hacia cero
sar eax, 2 ; Desplazamiento aritmético (división real)
2. División por constantes (No potencias de dos)
Para divisores que no son potencias de dos, el compilador emplea "Números Mágicos". Esta técnica se basa en la aproximación de la división mediante la multiplicación por el recíproco de la constante, escalado por una potencia de 2 ($1/d \approx M / 2^n$).
Caso: Divisor positivo (Ejemplo: valor / 5)
Para dividir por 5, el compilador podría utilizar el número mágico 0x66666667, que equivale aproximadamente a $2^{33}/5$.
// Lógica de desensamblado para valor / 5
mov eax, 0x66666667 ; Cargar número mágico
imul [ebp+arg_0] ; Multiplicación con signo (resultado en EDX:EAX)
sar edx, 1 ; Ajuste final del exponente
mov eax, edx
shr eax, 1Fh ; Extraer el bit de signo para corregir el resultado
add eax, edx ; EAX contiene ahora el cociente final
Caso: Divisor con ajuste de magnitud
Si el número mágico requerido es superior a 0x7FFFFFFF, el compilador suele añadir una instrucción ADD intermedia para compensar el desbordamiento de precisión antes del desplazamiento final.
3. División por números negativos
Cuando el divisor es negativo, la lógica sigue siendo similar a la de los números positivos, pero con un paso adicional de negación (NEG) o mediante el uso de un número mágico calculado específicamente para la magnitud negativa, siguiendo la fórmula $|M| = 2^n / (2^{32} - c)$.
4. Optimización de la operación módulo (%)
La operación de resto o módulo también se optimiza drásticamente. Si el divisor es una potencia de dos, se utiliza una máscara de bits con la instrucción AND.
// Ejemplo: valor % 8
// Para números positivos, esto es equivalente a: valor & 7
mov eax, [ebp+arg_0]
and eax, 80000007h ; Máscara para preservar el signo y el valor
jns label_positive ; Si es positivo, saltar ajuste
dec eax ; Ajuste para negativos
or eax, FFFFFFF8h
inc eax
label_positive:
; EAX contiene el resto
Si el divisor no es potancia de dos, el compilador primero calcula el cociente de la división (usando la optimización de multiplicación mencionada anteriormente), luego lo multiplica por el divisor y resta este resultado del dividendo original: $resto = dividendo - (cociente \times divisor)$.
Resumen de patrones para ingeniería inversa
- SAR: Indica división por potencias de 2.
- IMUL con constante: Indica división por una constante no potencia de 2.
- AND con (2^n - 1): Indica operación módulo con potencia de 2.
- LEA: Se utiliza a menudo después de una división para reconstruir el producto y calcular el residuo rápidamente.