Abstrações de custo zero são um dos pilares do design de Rust. A ideia é que você possa escrever código expressivo e de alto nível, usando recursos como closures, iteradores, traits e genéricos, sem pagar custo de desempenho em relação a uma implementação manual e de baixo nível. Em outras palavras, o compilador deve otimizar o código abstrato para que ele seja tão eficiente quanto a versão que você escreveria à mão, com ponteiros e loops explícitos.

Esse princípio foi herdado do C++, mas Rust o leva a sério com um sistema de tipos forte e um compilador agressivo em otimizações. Muitas vezes, o que parece uma camada extra de abstração (como usar Iterator em vez de um loop for com índices) é completamente eliminado pelo compilador, gerando código de máquina idêntico ao da versão manual. Isso permite que programadores escrevam código limpo e seguro sem sacrificar velocidade.

O que significa

Uma abstração de custo zero é uma construção de linguagem que não adiciona overhead em tempo de execução comparado a uma implementação manual equivalente. Em Rust, isso é alcançado através de:

  • Monoforização de genéricos: para cada tipo concreto usado, o compilador gera uma versão especializada do código, eliminando despacho dinâmico quando possível.
  • Inline e otimizações: funções pequenas e closures são frequentemente inline, evitando chamadas de função.
  • Iteradores: a API de iteradores é projetada para que o compilador possa eliminar a criação de estruturas intermediárias e loops aninhados.
  • Representações de dados eficientes: enums e structs são representados de forma compacta, e traits podem ser usados com despacho estático (monomorfização) ou dinâmico (trait objects) conforme a necessidade.

Na prática, isso significa que você pode usar abstrações sem medo de que elas tornem seu programa mais lento. O compilador cuida de transformar o código abstrato em instruções de máquina otimizadas.

Exemplos

Vamos ver um exemplo clássico: somar os quadrados de uma lista de números. Primeiro, uma implementação manual com loop e índice:

fn soma_quadrados_manual(lista: &[i32]) -> i32 {
    let mut soma = 0;
    for i in 0..lista.len() {
        let x = lista[i];
        soma += x * x;
    }
    soma
}

Agora, usando iteradores e closures:

fn soma_quadrados_iterador(lista: &[i32]) -> i32 {
    lista.iter().map(|x| x * x).sum()
}

Ambas as versões produzem o mesmo resultado, mas a segunda é muito mais expressiva. O compilador otimiza a versão com iteradores para um loop simples, sem criar vetores intermediários ou chamadas de closures em tempo de execução. O código de máquina resultante é praticamente idêntico ao da versão manual.

Outro exemplo é o uso de Option e Result com o operador ?. Em vez de verificar manualmente se um valor é None ou Err, o compilador gera código de desvio eficiente, sem custo adicional.

Iteradores vs loops

Muitos programadores vêm de linguagens onde loops são mais eficientes que abstrações, mas em Rust isso não é verdade. Os iteradores são projetados para serem tão rápidos quanto loops manuais, e muitas vezes mais seguros e legíveis. Vamos comparar um caso comum: encontrar o maior número em um vetor.

Com loop manual:

fn maior_manual(lista: &[i32]) -> i32 {
    let mut maior = lista[0];
    for i in 1..lista.len() {
        if lista[i] > maior {
            maior = lista[i];
        }
    }
    maior
}

Com iteradores:

fn maior_iterador(lista: &[i32]) -> i32 {
    lista.iter().cloned().max().unwrap()
}

O compilador transforma a versão com iteradores em um loop com uma comparação, sem criar coleções intermediárias. Além disso, os iteradores oferecem métodos como filter, map, fold, que podem ser combinados de forma declarativa, e o compilador os funde em um único loop otimizado.

Um erro comum é acreditar que for com índices é mais rápido que iteradores. Na verdade, acessar via índices pode ser mais lento porque o compilador precisa verificar os limites (bounds checking) em cada acesso, enquanto iteradores são implementados de forma que o compilador pode eliminar essas verificações. Portanto, iteradores são geralmente a melhor escolha em Rust.

Inspecionando o assembly (visão geral)

Para verificar que as abstrações de custo zero funcionam, podemos inspecionar o assembly gerado. O Rust fornece ferramentas para isso, como o cargo asm (com o plugin cargo-asm) ou usando o playground com a opção de mostrar o assembly. Vamos ver um exemplo simples.

Considere a função:

pub fn soma(lista: &[i32]) -> i32 {
    lista.iter().sum()
}

Se compilarmos com otimizações (--release), o assembly gerado será um loop simples que soma os elementos, sem chamadas de função extras. O compilador elimina toda a maquinaria dos iteradores.

Para ver isso na prática, você pode usar o Godbolt Compiler Explorer, selecionando Rust e compilando com -O. Você verá o assembly correspondente. Por exemplo, a função acima gera algo como:

; assembly simplificado (não é o real, mas ilustra)
loop:
    add eax, [rdi]
    add rdi, 4
    dec rcx
    jnz loop

Note que não há chamadas para funções de iterador, apenas um loop direto. Isso demonstra que a abstração foi eliminada.

Outra ferramenta é o cargo-asm, que permite ver o assembly de funções específicas. Instale com cargo install cargo-asm e use cargo asm para inspecionar.

Boas práticas

Algumas boas práticas para aproveitar as abstrações de custo zero:

  • Prefira iteradores e closures a loops manuais, a menos que haja uma razão clara para o contrário.
  • Evite coletar resultados intermediários (como collect()) desnecessariamente; use operações de consumo como sum(), count(), fold().
  • Aproveite a monomorfização: escreva código genérico, mas entenda que o compilador gera código específico para cada tipo.
  • Use #[inline] em funções pequenas quando necessário, mas confie que o compilador já fará isso automaticamente quando benéfico.
  • Meça o desempenho com benchmarks (criterion) para garantir que suas otimizações manuais realmente fazem diferença.

Referências

Exercícios

  1. Escreva uma função que receba um slice de inteiros e retorne a soma dos cubos dos elementos, usando iteradores. Compare com uma versão manual com loop.

    ✓ Resposta:
    fn soma_cubos(lista: &[i32]) -> i32 {
        lista.iter().map(|&x| x * x * x).sum()
    }
  2. Explique por que o código com iteradores pode ser mais rápido que um loop com índices em Rust.

    ✓ Resposta: O compilador pode eliminar verificações de limites (bounds checking) em iteradores, pois ele conhece os tamanhos e os acessos são feitos por ponteiros, resultando em código mais eficiente.
  3. Use o Compiler Explorer (Godbolt) para comparar o assembly de uma função com loop manual e uma com iteradores, ambas somando os elementos de um vetor. Descreva as diferenças que você observa.

    ✓ Resposta: Em geral, o assembly é quase idêntico, com um loop simples. A versão com iteradores não tem chamadas de função extras e o loop é o mesmo. Qualquer diferença é mínima e depende da otimização.
  4. O que é monomorfização e como ela contribui para abstrações de custo zero?

    ✓ Resposta: Monomorfização é o processo de gerar código específico para cada tipo concreto usado com genéricos. Isso elimina despacho dinâmico e permite otimizações específicas, tornando o código genérico tão rápido quanto código escrito manualmente para cada tipo.
  5. Escreva um código que use filter e fold para calcular a soma dos números pares de uma lista, e explique como o compilador pode otimizar isso.

    ✓ Resposta:
    let soma_pares = lista.iter().filter(|&&x| x % 2 == 0).fold(0, |acc, &x| acc + x);
    O compilador pode fundir o filter e o fold em um único loop, sem criar coleções intermediárias, resultando em código eficiente.