Démystifier les binaires BEAM : comment nous avons rendu notre bibliothèque DNS ultra-rapide (sans aucun NIF)
Lorsque vous gérez une plateforme qui traite l'enregistrement de domaines, l'hébergement DNS et l'infrastructure cloud comme nous le faisons chez NameOcean, la performance n'est pas qu'un simple bonus — c'est le fondement même de la fiabilité. C'est pourquoi nous sommes enthousiastes lorsque nous découvrons des histoires d'optimisation ingénieuses issues de l'écosystème Erlang, et c'est pourquoi nous avons voulu décortiquer l'une de nos préférées pour vous.
Récemment, nous sommes tombés sur une analyse approfondie fascinante de l'équipe DNSimple concernant leur travail d'optimisation de dns_erlang, leur bibliothèque DNS open-source. Leurs résultats étaient frappants : 12 % de CPU en moins par requête et 14 % de requêtes servies en plus par seconde — le tout grâce à de pures optimisations de la BEAM, sans aucun NIF requis. Pour quiconque construit des systèmes à haut débit en Elixir ou en Erlang, c'est une lecture essentielle.
Décortiquons ce qui rend cela possible.
Le modèle mental des binaires de la BEAM
Voici quelque chose qui surprend de nombreux développeurs Elixir : l'ajout (append) à des binaires dans une boucle serrée n'est pas aussi coûteux que vous ne le pensez.
Lorsque vous écrivez quelque chose comme :
def build_response(acc, []) do
acc
end
def build_response(acc, [h | t]) do
build_response(<<acc::binary, h>>, t)
end
Vous pourriez vous attendre à une complexité de O(n²) — chaque itération créant un nouveau binaire et copiant tout le contenu. Mais la BEAM est plus intelligente que cela.
Comme les binaires sont immuables, le compilateur peut prouver que si acc vient d'être créé par un ajout précédent et qu'aucune autre référence ne le détient, écrire directement dans cette mémoire existante est sûr. Le premier ajout alloue une certaine capacité de réserve, et les ajouts suivants écrivent dedans. Le résultat ? Un coût amorti de O(1) par ajout — identique au fonctionnement d'un std::vector en C++.
Le piège ? Si votre binaire « s'échappe » — c'est-à-dire s'il est stocké dans un dictionnaire de processus, renvoyé à un appelant ou ajouté à autre chose — la fête est finie. Vous revenez à la phase de copie.
Ce schéma se retrouve partout dans la gestion des protocoles binaires. Lorsque vous construisez des réponses DNS de manière itérative, maintenir ce flux d'accumulateur serré est d'une importance capitale.
Sous-binaires : le découpage gratuit (en grande partie)
Le pattern matching sur les binaires crée ce que la BEAM appelle des sous-binaires — de minuscules termes qui pointent vers les données originales sans les copier. Lorsque vous écrivez :
<<Header:12/binary, Rest/binary>> = Packet
Aucune donnée n'est copiée. Rest n'est qu'une fenêtre sur le paquet d'origine.
Mais voici la subtilité qui piège les utilisateurs : la BEAM conserve un contexte de correspondance interne lorsque vous effectuez du matching récursif, essentiellement un curseur parcourant les octets. Elle ne matérialisera un sous-binaire que si vous le laissez réellement s'échapper — en le renvoyant, en le stockant ou en le passant là où il pourrait persister.
Cela compte énormément pour le parsing DNS. Chaque requête arrive sous forme de binaire, est comparée à la structure du protocole, et doit produire une réponse. Comprendre quand vous créez du travail supplémentaire plutôt que de simplement visualiser les données est la différence entre un parseur qui passe à l'échelle et un autre qui s'étouffe sous la charge.
Pourquoi la représentation des entiers importe plus que vous ne le pensez
Sur une BEAM 64 bits, les entiers utilisent quelques bits pour leur étiquette de type. Il reste donc 60 bits pour la valeur réelle. Les entiers qui tiennent dans cet espace sont des « petits entiers » (small integers) — des valeurs immédiates qui résident directement dans les registres du processus, sans jamais toucher le tas (heap).
Tout ce qui est plus grand devient un « bignum », ce qui nécessite une allocation sur le tas et une gestion par le ramasse-miettes (garbage collector). Cela semble ésotérique, mais cela devient critique lorsque vous effectuez le type de manipulation de bits exigé par le travail sur les protocoles binaires.
La technique est appelée SWAR (SIMD Within A Register) — l'empaquetage de plusieurs valeurs dans un seul entier afin de pouvoir les traiter en parallèle au niveau du CPU. Pour le DNS, cela signifie analyser de nombreux octets de données à chaque opération. Mais cela ne fonctionne que si vos valeurs empaquetées tiennent dans la zone idéale des « petits entiers ».
Lorsqu'elles ne tiennent pas, vous ne payez pas seulement des coûts d'allocation mémoire — vous déclenchez potentiellement des cycles de ramasse-miettes qui n'existeraient pas autrement. À des millions de requêtes par seconde, ces cycles s'accumulent.
Ce que cela signifie pour votre code
Voici la conclusion pratique : vous n'avez pas besoin d'écrire des NIF ou de vous tourner vers le C pour obtenir des gains de performance sérieux. Vous devez comprendre comment la BEAM traite les données.
Quelques modèles à internaliser :
1. Gardez les accumulateurs binaires serrés. Les boucles d'ajout uniquement sont vos alliées. Une fois qu'un binaire s'échappe, le modèle de coût change radicalement.
2. Faites du matching sur place quand c'est possible. Utilisez le pattern matching pour visualiser les données plutôt que pour les copier. La BEAM est optimisée pour cela.
3. Surveillez la taille de vos entiers. Lors d'opérations arithmétiques ou de manipulation de bits, demandez-vous si vos valeurs resteront dans la plage des « petits entiers ».
4. Profilez en gardant les réductions à l'esprit. La BEAM planifie le travail en « réductions » (approximativement des appels de fonction). Une boucle intensive qui effectue plus de matching avec moins d'appels de fonction génère moins de travail du point de vue de l'ordonnanceur.
Pour quiconque construit des systèmes gérant des protocoles binaires — qu'il s'agisse de DNS, de trames WebSocket ou d'implémentations de protocoles personnalisés — ces modèles s'appliquent directement. La BEAM vous offre des outils incroyables pour écrire du code rapide, mais ils ne sont utiles que si vous comprenez le modèle.
Nous avons vu des équipes se tourner vers des NIF en C ou en Rust alors qu'une compréhension plus profonde de la sémantique de la BEAM leur aurait permis d'obtenir 90 % des gains sans aucune de la complexité. Le runtime est plus intelligent qu'on ne le croit souvent — notre travail est d'écrire du code qui lui permet de s'exprimer.
Chez NameOcean, nous pensons toujours à la performance à grande échelle. Que vous lanciez Vibe Hosting pour votre prochain projet ou que vous gériez le DNS d'une application à fort trafic, comprendre ces fondamentaux vous aide à construire des systèmes qui restent rapides lorsque l'activité s'intensifie.
Bon code, et que vos binaires s'ajoutent toujours avec efficacité.