Desmitificando los binarios de BEAM: Cómo hicimos que nuestra librería DNS fuera ultrarrápida (sin usar NIFs)
Cuando gestionas una plataforma que maneja el registro de dominios, el alojamiento DNS y la infraestructura en la nube como hacemos en NameOcean, el rendimiento no es solo un extra agradable: es la base de la fiabilidad. Por eso nos entusiasma ver historias de optimización ingeniosas del ecosistema Erlang, y por eso queremos desglosar una de nuestras favoritas para ti.
Recientemente, nos topamos con un fascinante análisis profundo del equipo de DNSimple sobre su trabajo optimizando dns_erlang, su biblioteca DNS de código abierto. Sus resultados fueron sorprendentes: un 12% menos de CPU por consulta y un 14% más de consultas servidas por segundo, todo mediante optimizaciones puras de la BEAM, sin necesidad de NIFs. Para cualquiera que esté construyendo sistemas de alto rendimiento en Elixir o Erlang, esta es una lectura esencial.
Vamos a desglosar qué es lo que lo hace posible.
El modelo mental de los binarios en la BEAM
Aquí hay algo que sorprende a muchos desarrolladores de Elixir: añadir elementos a binarios en un bucle cerrado no es tan costoso como podrías pensar.
Cuando escribes algo como:
def build_response(acc, []) do
acc
end
def build_response(acc, [h | t]) do
build_response(<<acc::binary, h>>, t)
end
Podrías esperar que esto sea O(n²) —cada iteración creando un nuevo binario y copiando todo—. Pero la BEAM es más inteligente que eso.
Debido a que los binarios son inmutables, el compilador puede demostrar que si acc acaba de ser creado por un "append" previo y nada más mantiene una referencia a él, escribir directamente en esa memoria existente es seguro. El primer "append" asigna algo de capacidad de reserva, y los "appends" subsiguientes escriben en ella. ¿El resultado? Un O(1) amortizado por "append", idéntico a cómo funciona un std::vector en C++.
¿El truco? Si tu binario "escapa" —es decir, si se almacena en un diccionario de proceso, se devuelve a un llamador o se añade a otra cosa— la fiesta se acaba. Vuelves a la fase de copiado.
Este patrón aparece en todas partes en el manejo de protocolos binarios. Cuando estás construyendo respuestas DNS de forma iterativa, mantener ese flujo de acumulador compacto es enormemente importante.
Sub-binarios: Slicing gratuito (en su mayoría)
El "matching" en binarios crea lo que la BEAM llama sub-binarios: términos diminutos que apuntan a los datos originales sin copiarlos. Cuando escribes:
<<Header:12/binary, Rest/binary>> = Packet
No se copia ningún dato. Rest es solo una ventana al paquete original.
Pero aquí está la sutileza que confunde a la gente: la BEAM mantiene un contexto de coincidencia internamente cuando haces matching recursivo, esencialmente un cursor que recorre los bytes. No materializará un sub-binario a menos que realmente permitas que escape: devolviéndolo, almacenándolo o pasándolo a algún lugar donde pueda permanecer.
Esto importa enormemente para el análisis (parsing) de DNS. Cada consulta llega como un binario, se compara con la estructura del protocolo y necesita producir una respuesta. Entender cuándo estás creando trabajo frente a cuándo solo estás visualizando datos es la diferencia entre un parser que escala y uno que se asfixia bajo la carga.
Por qué la representación de enteros importa más de lo que crees
En una BEAM de 64 bits, los enteros ocupan algunos bits para su etiqueta de tipo. Eso deja 60 bits para el valor real. Los enteros que caben en ese espacio son "enteros pequeños" (small integers): valores inmediatos que viven directamente en los registros del proceso, sin tocar nunca el heap.
Cualquier cosa más grande se convierte en un "bignum", lo que requiere asignación de memoria en el heap y consideración de recolección de basura (garbage collection). Esto suena esotérico, pero se vuelve crítico cuando estás haciendo el tipo de manipulación de bits que exige el trabajo con protocolos binarios.
La técnica se llama SWAR (SIMD Within A Register): empaquetar múltiples valores en un solo entero para poder operar sobre ellos en paralelo a nivel de CPU. Para DNS, esto significa analizar muchos bytes de datos en cada operación. Pero solo funciona si tus valores empaquetados caben en el punto ideal de los "enteros pequeños".
Cuando no es así, no solo estás pagando costos de asignación de memoria, sino que potencialmente estás activando ciclos de recolección de basura que no existirían de otro modo. A millones de consultas por segundo, esos ciclos se acumulan.
Qué significa esto para tu código
Aquí está la conclusión práctica: no necesitas escribir NIFs ni recurrir a C para obtener ganancias de rendimiento serias. Necesitas entender cómo piensa la BEAM sobre los datos.
Algunos patrones que vale la pena internalizar:
1. Mantén los acumululadores de binarios compactos. Los bucles de solo añadir (append-only) son tus amigos. Una vez que un binario escapa, el modelo de costos cambia drásticamente.
2. Haz matching in situ cuando sea posible. Usa el "pattern matching" para visualizar los datos en lugar de copiarlos. La BEAM está optimizada para esto.
3. Vigila el tamaño de tus enteros. Al hacer aritmética o manipulación de bits, piensa en si tus valores se mantendrán en el rango de "enteros pequeños".
4. Perfila teniendo en cuenta las reducciones. La BEAM programa el trabajo en reducciones (aproximadamente llamadas a funciones). Un bucle caliente que haga más matching en menos llamadas a funciones realiza menos trabajo desde la perspectiva del planificador (scheduler).
Para cualquiera que esté construyendo sistemas que manejen protocolos binarios —ya sea DNS, tramas WebSocket o implementaciones de protocolos personalizados— estos patrones se aplican directamente. La BEAM te ofrece herramientas increíbles para escribir código rápido, pero solo son útiles si entiendes el modelo.
Hemos visto equipos recurrir a NIFs de C o de Rust cuando una comprensión más profunda de la semántica de la BEAM les habría dado el 90% de las ganancias sin nada de la complejidad. El runtime es más inteligente de lo que solemos creer; nuestro trabajo es escribir código que le permita lucirse.
En NameOcean, siempre estamos pensando en el rendimiento a escala. Ya sea que estés lanzando Vibe Hosting para tu próximo proyecto o gestionando el DNS para una aplicación de alto tráfico, comprender estos fundamentos te ayuda a construir sistemas que se mantienen rápidos cuando las cosas se complican.
Feliz programación, y que tus binarios siempre se añadan con eficiencia.