Segredo do i128 em sistemas de 64 bits: como o Rust trabalha com números gigantescos

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Introdução: Números grandes em um mundo pequeno



Imagine: você está escrevendo um algoritmo criptográfico, uma simulação de processos físicos ou simplesmente calculando a distância até a galáxia mais próxima em nanômetros. E de repente percebe que o padrão i64 (de -9 quintilhões a 9 quintilhões) é catastroficamente insuficiente. No Rust, há uma tábua de salvação — o tipo i128. Mas como ele funciona em um processador que fisicamente não sabe trabalhar com números de 128 bits? Não é mágica?



Spoiler: não há mágica, há engenharia genial. Neste artigo, vamos analisar como o Rust (ou melhor, o LLVM sob o capô) emula i128 em uma arquitetura de 64 bits. Você descobrirá por que isso não é tão lento quanto parece e como escrever código eficiente com esses números gigantescos. Não é teoria seca — percorreremos o caminho da pergunta até a aplicação real com exemplos.



1. Realidade do hardware: por que 128 bits é um desafio



Os processadores modernos de 64 bits (x86-64, ARM64) possuem registradores de uso geral com tamanho de 64 bits. Este é o seu tamanho de palavra "nativo". Para trabalhar com um número de 128 bits, o processador deve executar várias operações sobre duas metades de 64 bits: a parte baixa (low) e a parte alta (high).



No Rust, o tipo i128 (e seu irmão sem sinal u128) não é um tipo de hardware, mas sim um tipo agregado. Ele é armazenado na memória como duas palavras de 64 bits adjacentes. O compilador (através do LLVM) gera código que processa essas palavras como uma unidade única.



Um exemplo simples:



fn main() {    let big: i128 = 123456789012345678901234567890;    println!("{}", big);}


No nível do assembly, isso se transformará em uma sequência de instruções que trabalham com dois registradores de 64 bits (por exemplo, rax e rdx). O LLVM sabe como "colar" essas operações de forma muito eficiente.



2. Anatomia da adição: como o Rust soma i128



Vamos dar uma olhada na operação mais básica — a adição de dois i128. Escreveremos uma função simples e examinaremos sob o capô com cargo-show-asm ou Compiler Explorer.



fn add_big(a: i128, b: i128) -> i128 {    a + b}


Vamos compilar para x86-64 (sem otimizações, para ficar mais claro). O LLVM gerará aproximadamente este pseudocódigo:



  1. Carregar os 64 bits baixos de a no registrador rax, os altos em rdx.
  2. Carregar os 64 bits baixos de b em rbx, os altos em rcx.
  3. Somar as partes baixas: add rax, rbx — resultado em rax, a flag de carry (CF) é definida se houve estouro.
  4. Somar as partes altas considerando o carry: adc rdx, rcx — adc (add with carry) adiciona 1 se a flag CF foi definida.
  5. Resultado: 64 bits baixos em rax, 64 bits altos em rdx.


Isso é a emulação. Duas instruções em vez de uma. Para multiplicação e divisão, o código será mais complexo, mas o princípio é o mesmo — divisão em operações de 64 bits.



3. E quanto ao desempenho? Mitos e realidade



Muitos pensam que i128 é duas vezes mais lento que i64. Isso não é totalmente verdade. Sim, cada operação se transforma em 2-3 instruções em vez de uma. Mas os processadores modernos possuem:



  • Pipelines e superescalaridade — podem executar várias instruções por ciclo.
  • Predição de desvios — especialmente importante para comparações.
  • Grandes caches — o trabalho com a memória não sofre, pois i128 ocupa apenas 16 bytes.


Teste prático (Rust, aritmética simples em loop):



use std::time::Instant;

fn main() { let n = 100_000_000; // i64 let start = Instant::now(); let m

Blogs

Book Recommendations