Desmistificando Binários BEAM: Como Tornamos Nossa Biblioteca DNS Extremamente Rápida (Sem Nenhuma NIF)
Quando você está operando uma plataforma que lida com registro de domínios, hospedagem DNS e infraestrutura em nuvem como fazemos na NameOcean, o desempenho não é apenas um "desejo" — é a base da confiabilidade. É por isso que ficamos entusiasmados quando vemos histórias de otimização inteligentes do ecossistema Erlang, e por isso queremos detalhar uma das nossas favoritas para você.
Recentemente, nos deparamos com um mergulho profundo fascinante da equipe da DNSimple sobre o trabalho deles otimizando o dns_erlang, sua biblioteca DNS de código aberto. Os resultados foram impressionantes: 12% menos CPU por consulta e 14% mais consultas atendidas por segundo — tudo através de puras otimizações da BEAM, sem a necessidade de NIFs. Para quem está construindo sistemas de alto rendimento em Elixir ou Erlang, esta é uma leitura essencial.
Vamos desvendar o que torna isso possível.
O Modelo Mental de Binários da BEAM
Aqui está algo que surpreende muitos desenvolvedores Elixir: anexar (append) a binários em um loop apertado não é tão caro quanto você imagina.
Quando você escreve algo como:
def build_response(acc, []) do
acc
end
def build_response(acc, [h | t]) do
build_response(<<acc::binary, h>>, t)
end
Você pode esperar que isso seja O(n²) — cada iteração criando um novo binário e copiando tudo. Mas a BEAM é mais inteligente que isso.
Como os binários são imutáveis, o compilador pode provar que, se o acc foi acabado de criar por um append anterior e nada mais detém uma referência a ele, escrever diretamente naquela memória existente é seguro. O primeiro append aloca uma certa capacidade extra, e os appends subsequentes escrevem nela. O resultado? O(1) amortizado por append — idêntico ao modo como um std::vector funciona em C++.
O problema? Se o seu binário "escapar" — o que significa que ele é armazenado em um dicionário de processo, retornado para um chamador ou anexado a algo mais — a festa acaba. Você volta a copiar.
Esse padrão aparece em todo lugar no tratamento de protocolos binários. Quando você está construindo respostas DNS de forma iterativa, manter esse fluxo de acumulador compacto é extremamente importante.
Sub-binários: Fatiamento Gratuito (Na Maioria das Vezes)
O pattern matching em binários cria o que a BEAM chama de sub-binários — termos minúsculos que apontam para os dados originais sem copiá-los. Quando você escreve:
<<Header:12/binary, Rest/binary>> = Packet
Nenhum dado é copiado. Rest é apenas uma janela para o pacote original.
Mas aqui está a sutileza que confunde as pessoas: a BEAM mantém um contexto de correspondência internamente quando você faz o match recursivamente, essencialmente um cursor percorrendo os bytes. Ela não materializará um sub-binário a menos que você realmente o deixe escapar — retornando-o, armazenando-o ou passando-o para algum lugar onde ele possa permanecer.
Isso importa enormemente para o parsing de DNS. Cada consulta chega como um binário, é comparada com a estrutura do protocolo e precisa produzir uma resposta. Entender quando você está criando trabalho versus apenas visualizando dados é a diferença entre um parser que escala e um que engasga sob carga.
Por que a Representação de Inteiros Importa Mais do que Você Pensa
Em uma BEAM de 64 bits, os inteiros ocupam alguns bits para sua tag de tipo. Isso deixa 60 bits para o valor real. Inteiros que cabem nesse espaço são "inteiros pequenos" (small integers) — valores imediatos que vivem diretamente nos registradores do processo, nunca tocando o heap.
Qualquer coisa maior torna-se um bignum, o que requer alocação de heap e consideração de coleta de lixo (garbage collection). Isso parece esotérico, mas torna-se crítico quando você está fazendo o tipo de manipulação de bits que o trabalho com protocolos binários exige.
A técnica é chamada de SWAR (SIMD Within A Register) — empacotar múltiplos valores em um único inteiro para que você possa operar sobre eles em paralelo ao nível da CPU. Para DNS, isso significa processar muitos bytes de dados em cada operação. Mas isso só funciona se seus valores empacotados couberem no "ponto ideal" do "inteiro pequeno".
Quando não cabem, você não está apenas pagando custos de alocação de memória — você está potencialmente disparando ciclos de coleta de lixo que não existiriam de outra forma. Em milhões de consultas por segundo, esses ciclos se acumulam.
O Que Isso Significa para o Seu Código
Aqui está a lição prática: você não precisa escrever NIFs ou recorrer ao C para obter ganhos de desempenho sérios. Você precisa entender como a BEAM pensa sobre os dados.
Alguns padrões que valem a pena internalizar:
1. Mantenha os acumuladores de binários compactos. Loops de "apenas append" são seus amigos. Assim que um binário escapa, o modelo de custo muda drasticamente.
2. Faça o match no local sempre que possível. Use o pattern matching para visualizar os dados em vez de copiá-los. A BEAM é otimizada para isso.
3. Cuidado com o tamanho dos seus inteiros. Ao fazer aritmética ou manipulação de bits, pense se seus valores permanecerão na faixa de "inteiro pequeno".
4. Faça o profiling com foco em reduções. A BEAM agenda o trabalho em reduções (aproximadamente chamadas de função). Um loop quente que faz mais matching em menos chamadas de função realiza menos trabalho do ponto de vista do escalonador.
Para quem constrói sistemas que lidam com protocolos binários — seja DNS, frames de WebSocket ou implementações de protocolos customizados — esses padrões se aplicam diretamente. A BEAM oferece ferramentas incríveis para escrever código rápido, mas elas só são úteis se você entender o modelo.
Já vimos equipes recorrerem a NIFs ou NIFs em Rust quando um entendimento mais profundo da semântica da BEAM teria proporcionado 90% dos ganhos sem nenhuma da complexidade. O runtime é mais inteligente do que costumamos dar crédito — nosso trabalho é escrever código que permita que ele se destaque.
Na NameOcean, estamos sempre pensando em desempenho em escala. Esteja você configurando o Vibe Hosting para seu próximo projeto ou gerenciando o DNS para uma aplicação de alto tráfego, entender esses fundamentos ajuda você a construir sistemas que permanecem rápidos quando as coisas ficam intensas.
Boa programação, e que seus binários sempre façam append de forma eficiente.