Para proporcionar quizás un ejemplo más claro, en x86_64, compilado con la -O
bandera, la función
pub fn leet(a : i128) -> i128 {
a + 1337
}
compila a
example::leet:
mov rdx, rsi
mov rax, rdi
add rax, 1337
adc rdx, 0
ret
(Mi publicación original tenía u128
más de lo i128
que pediste. La función compila el mismo código de cualquier manera, una buena demostración de que la adición firmada y no firmada es la misma en una CPU moderna).
La otra lista produjo código no optimizado. Es seguro avanzar en un depurador, porque se asegura de que pueda poner un punto de interrupción en cualquier lugar e inspeccionar el estado de cualquier variable en cualquier línea del programa. Es más lento y más difícil de leer. La versión optimizada está mucho más cerca del código que realmente se ejecutará en producción.
El parámetro a
de esta función se pasa en un par de registros de 64 bits, rsi: rdi. El resultado se devuelve en otro par de registros, rdx: rax. Las dos primeras líneas de código inicializan la suma a a
.
La tercera línea agrega 1337 a la palabra baja de la entrada. Si esto se desborda, lleva el 1 en el indicador de acarreo de la CPU. La cuarta línea agrega cero a la palabra alta de la entrada, más el 1 si se transfirió.
Puede pensar en esto como una simple adición de un número de un dígito a un número de dos dígitos
a b
+ 0 7
______
pero en la base 18,446,744,073,709,551,616. Todavía está agregando el "dígito" más bajo primero, posiblemente llevando un 1 a la siguiente columna, luego agregando el siguiente dígito más el acarreo. La resta es muy similar.
La multiplicación debe usar la identidad (2⁶⁴a + b) (2⁶⁴c + d) = 2¹²⁸ac + 2⁶⁴ (ad + bc) + bd, donde cada una de estas multiplicaciones devuelve la mitad superior del producto en un registro y la mitad inferior del producto en otro. Algunos de esos términos se descartarán, porque los bits por encima del 128 no caben en u128
ay se descartan. Aun así, esto requiere una serie de instrucciones de la máquina. La división también toma varios pasos. Para un valor con signo, la multiplicación y la división necesitarían además convertir los signos de los operandos y el resultado. Esas operaciones no son muy eficientes en absoluto.
En otras arquitecturas, se vuelve más fácil o más difícil. RISC-V define una extensión de conjunto de instrucciones de 128 bits, aunque que yo sepa, nadie la ha implementado en silicio. Sin esta extensión, el manual de arquitectura RISC-V recomienda una rama condicional:addi t0, t1, +imm; blt t0, t1, overflow
SPARC tiene códigos de control como los indicadores de control de x86, pero debe usar una instrucción especial add,cc
para configurarlos. MIPS, por otro lado, requiere que verifique si la suma de dos enteros sin signo es estrictamente menor que uno de los operandos. Si es así, la adición se desbordó. Al menos puede establecer otro registro al valor del bit de acarreo sin una rama condicional.