Para fornecer talvez um exemplo mais claro, em x86_64, compilado com o -O
sinalizador, a função
pub fn leet(a : i128) -> i128 {
a + 1337
}
compila para
example::leet:
mov rdx, rsi
mov rax, rdi
add rax, 1337
adc rdx, 0
ret
(Minha postagem original tinha u128
mais do que i128
você perguntou. A função compila o mesmo código de qualquer maneira, uma boa demonstração de que adição assinada e não assinada é a mesma em uma CPU moderna.)
A outra listagem produziu código não otimizado. É seguro avançar em um depurador, porque garante que você possa colocar um ponto de interrupção em qualquer lugar e inspecionar o estado de qualquer variável em qualquer linha do programa. É mais lento e mais difícil de ler. A versão otimizada está muito mais próxima do código que realmente será executado na produção.
O parâmetro a
dessa função é passado em um par de registradores de 64 bits, rsi: rdi. O resultado é retornado em outro par de registros, rdx: rax. As duas primeiras linhas de código inicializam a soma para a
.
A terceira linha adiciona 1337 à palavra baixa da entrada. Se isso exceder, ele carrega o 1 no sinalizador de transporte da CPU. A quarta linha adiciona zero à palavra mais alta da entrada - mais 1 se for carregada.
Você pode pensar nisso como simples adição de um número de um dígito a um número de dois dígitos
a b
+ 0 7
______
mas na base 18.446.744.073.709.551.616. Você ainda está adicionando o "dígito" mais baixo primeiro, possivelmente carregando um 1 para a próxima coluna e, em seguida, adicionando o próximo dígito mais o transporte. Subtração é muito semelhante.
A multiplicação deve usar a identidade (2⁶⁴a + b) (2⁶⁴c + d) = 2¹²⁸ac + 2⁶⁴ (ad + bc) + bd, em que cada uma dessas multiplicações retorna a metade superior do produto em um registro e a metade inferior do produto em outro. Alguns desses termos serão descartados, porque os bits acima do 128º não cabem no a u128
e são descartados. Mesmo assim, são necessárias várias instruções da máquina. A divisão também dá vários passos. Para um valor assinado, multiplicação e divisão precisariam adicionalmente converter os sinais dos operandos e o resultado. Essas operações não são muito eficientes.
Em outras arquiteturas, fica mais fácil ou mais difícil. O RISC-V define uma extensão de conjunto de instruções de 128 bits, embora eu saiba que ninguém a implementou em silício. Sem essa extensão, o manual de arquitetura do RISC-V recomenda uma ramificação condicional:addi t0, t1, +imm; blt t0, t1, overflow
O SPARC possui códigos de controle como os sinalizadores de controle do x86, mas você precisa usar uma instrução especial add,cc
, para defini-los. O MIPS, por outro lado, exige que você verifique se a soma de dois números inteiros não assinados é estritamente menor que um dos operandos. Nesse caso, a adição estourou. Pelo menos você pode definir outro registro para o valor do bit de transporte sem uma ramificação condicional.