Demistificare i binari BEAM: come abbiamo reso la nostra libreria DNS velocissima (senza alcuna NIF)

Demistificare i binari BEAM: come abbiamo reso la nostra libreria DNS velocissima (senza alcuna NIF)

Ott 20, 2026 beam elixir erlang dns performance optimization binary protocols backend development

Quando una piattaforma gestisce la registrazione dei domini, l’hosting DNS e l’infrastruttura cloud, come nel nostro caso in NameOcean, le prestazioni non sono un semplice vantaggio: costituiscono la base dell’affidabilità. Ecco perché ci entusiasmo quando vediamo storie ingegnose di ottimizzazione dall’ecosistema Erlang e vogliamo analizzare per voi una delle nostre preferite.

Recentemente ci siamo imbattuti in un approfondimento affascinante del team DNSimple sul lavoro di ottimizzazione di dns_erlang, la loro libreria DNS open source. I risultati sono stati notevoli: il 12% di CPU in meno per query e il 14% di query in più servite al secondo, ottenuti interamente con ottimizzazioni del BEAM, senza alcuna NIF. Per chiunque stia sviluppando sistemi ad alto throughput in Elixir o Erlang, si tratta di una lettura essenziale.

Vediamo nel dettaglio che cosa rende tutto questo possibile.

Il modello mentale dei binari BEAM

Ecco un aspetto che surprises molti sviluppatori Elixir: aggiungere dati a un binario all’interno di un ciclo iterativo non costa quanto potresti aspettarti.

Quando scrivi qualcosa come:

def build_response(acc, []) do
  acc
end
def build_response(acc, [h | t]) do
  build_response(<<acc::binary, h>>, t)
end

Potresti aspettarti un comportamento O(n²), in cui ogni iterazione crea un nuovo binario e copia tutto il contenuto. Ma il BEAM è più intelligente di così.

Poiché i binari sono immutabili, il compilatore può dimostrare che, se acc è stato appena creato da un append precedente e nessun altro detiene un riferimento a esso, scrivere direttamente nella memoria esistente è sicuro. Il primo append alloca una capacità di riserva, mentre i successivi vi scrivono al suo interno. Il risultato? Un costo O(1) amortizzato per append, esattamente come accade con uno std::vector in C++.

La complicazione? Se il binario "sfugge" — cioè se viene memorizzato in un process dictionary, restituito al chiamante o concatenato a un altro valore — la festa è finita: si torna a copiare.

Questo pattern è diffuso ovunque nella gestione dei protocolli binari. Quando si costruiscono risposte DNS in modo iterativo, mantenere sotto stretto controllo il flusso dell’accumulatore è estremamente importante.

Hai un'idea? Trova il dominio perfetto per lei

Il nostro generatore di nomi di dominio con AI suggerisce nomi disponibili tra centinaia di estensioni. SSL, DNS e privacy Whois gratuiti inclusi.

Sub-binaries: slicing gratuito (quasi)

Il pattern matching sui binari crea ciò che il BEAM chiama sub-binaries: termini piccolissimi che puntano ai dati originali senza effettuare copie. Quando scrivi:

<<Header:12/binary, Rest/binary>> = Packet

Nessun dato viene copiato. Rest è soltanto una finestra sul pacchetto originale.

Ma c’è una sottigliezza che può trarre in inganno: quando si esegue il matching ricorsivo, il BEAM mantiene internamente un match context, in sostanza un cursore che percorre i byte. Non materializza un sub-binary a meno che non gli venga consentito di "sfuggire", ovvero restituirlo, memorizzarlo o passarlo a un componente che potrebbe trattenerlo.

Questo aspetto è decisivo nel parsing DNS. Ogni query arriva sotto forma di binario, viene sottoposta a matching rispetto alla struttura del protocollo e deve produrre una risposta. Capire quando si sta creando lavoro e quando si sta soltanto osservando i dati è la differenza tra un parser che scala e uno che non regge il carico.

Perché la rappresentazione degli interi conta più di quanto sembri

Su un BEAM a 64 bit, gli interi occupano pochi bit per il proprio type tag. Restano così 60 bit per il valore effettivo. Gli interi che rientrano in questo intervallo sono detti "small integers": valori immediati che risiedono direttamente nei registri del processo, senza mai toccare l’heap.

Qualsiasi valore più grande diventa un bignum: richiede un’allocazione sull’heap e deve essere preso in considerazione dalla garbage collection. Può sembrare un argomento specialistico, ma diventa fondamentale quando si eseguono le manipolazioni bit a bit richieste dal lavoro sui protocolli binari.

La tecnica si chiama SWAR (SIMD Within A Register): consiste nell’inserire più valori in un unico intero per potervi operare in parallelo a livello di CPU. Per il DNS, ciò significa eseguire il parsing di una quantità di dati pari a molti byte in ogni operazione. Funziona però soltanto se i valori impacchettati rientrano nella zona d’oro dei "small integer".

Quando non rientrano, non ci si limita a sostenere i costi di allocazione della memoria: si rischia di innescare cicli di garbage collection che altrimenti non si verificherebbero. A milioni di query al secondo, questi cicli si sommano.

Che cosa significa per il tuo codice

Il consiglio pratico è questo: per ottenere seri incrementi di prestazioni non serve scrivere una NIF o ricorrere al C. Serve capire come il BEAM ragiona sui dati.

Alcuni pattern da tenere a mente:

1. Tenere gli accumulatori binari sotto stretto controllo. I cicli basati solo su append sono una scelta particolarmente efficace. Una volta che un binario sfugge, il modello dei costi cambia in modo drammatico.

2. Fare il matching sul posto quando possibile. Usare il pattern matching per osservare i dati anziché copiarli. Il BEAM è ottimizzato per questo.

3. Controllare le dimensioni degli interi. Quando si eseguono operazioni aritmetiche o manipolazioni di bit, valutare se i valori resteranno nell’intervallo degli "small integer".

4. Eseguire il profiling tenendo conto delle reductions. Il BEAM pianifica il lavoro in reductions (grossomodo, in chiamate di funzione). Un ciclo critico che esegue più operazioni di matching ricorrendo a un numero inferiore di chiamate di funzione svolge meno lavoro dal punto di vista dello scheduler.

Questi pattern si applicano direttamente a chiunque sviluppi sistemi che gestiscono protocolli binari, che si tratti di DNS, frame WebSocket o implementazioni di protocolli personalizzati. Il BEAM offre strumenti straordinari per scrivere codice veloce, ma sono efficaci solo se se ne comprende il modello.

Abbiamo visto team ricorrere a una NIF, anche scritta in Rust, quando una comprensione più approfondita della semantica del BEAM avrebbe potuto consentire loro di ottenere il 90% dei miglioramenti senza alcuna complessità aggiuntiva. Il runtime è più intelligente di quanto spesso gli diamo credito: il nostro compito è scrivere codice che gli consenta di dare il meglio di sé.

In NameOcean pensiamo sempre alle prestazioni su scala. Che si tratti dell’avvio di Vibe Hosting per il prossimo progetto o della gestione del DNS di un’applicazione ad alto traffico, la comprensione di questi fondamenti aiuta a costruire sistemi che restano veloci quando l’attività aumenta.

Buon divertimento a programmare, e che possiate sempre accodare dati ai binari in modo efficiente.

Hai un'idea? Trova il dominio perfetto per lei

Il nostro generatore di nomi di dominio con AI suggerisce nomi disponibili tra centinaia di estensioni. SSL, DNS e privacy Whois gratuiti inclusi.

Leggi in altre lingue:

BG CS DA DE EL EN ES FI FR HU NB NL PL PT RO RU SV TR UZ ZH-HANS