Demistyfikacja binariów BEAM: Jak sprawiliśmy, że nasza biblioteka DNS stała się błyskawiczna (bez użycia NIF-ów)

Demistyfikacja binariów BEAM: Jak sprawiliśmy, że nasza biblioteka DNS stała się błyskawiczna (bez użycia NIF-ów)

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

Kiedy zarządzasz platformą obsługującą rejestrację domen, hosting DNS i infrastrukturę chmurową, tak jak my w NameOcean, wydajność to nie tylko miły dodatek – to fundament niezawodności. Dlatego z entuzjazmem podchodzimy do pomysłowych historii optymalizacji z ekosystemu Erlang i dlatego chcemy przedstawić Wam jedną z naszych ulubionych.

Ostatnio trafiliśmy na fascynujące, szczegółowe opracowanie zespołu DNSimple dotyczące ich pracy nad optymalizacją dns_erlang, ich biblioteki DNS typu open-source. Ich wyniki były uderzające: o 12% mniej zużycia CPU na zapytanie i o 14% więcej obsłużonych zapytań na sekundę – a wszystko to dzięki czystej optymalizacji BEAM, bez potrzeby stosowania NIF-ów. Dla każdego, kto buduje systemy o wysokiej przepustowości w Elixirze lub Erlangu, jest to lektura obowiązkowa.

Rozłóżmy to na czynniki pierwsze, aby zrozumieć, co za tym stoi.

Model mentalny binariów w BEAM

Oto coś, co zaskakuje wielu programistów Elixira: dopisywanie do binariów w ciasnej pętli nie jest tak kosztowne, jak mogłoby się wydawać.

Kiedy piszesz coś takiego:

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

Możesz oczekiwać złożoności O(n²) – każda iteracja tworzy nowy binarny i kopiuje wszystko. Ale BEAM jest sprytniejszy.

Ponieważ binarne są niezmienne (immutable), kompilator może udowodnić, że jeśli acc zostało właśnie utworzone przez poprzednie dopisanie i nic innego nie przechowuje do niego referencji, to zapis bezpośrednio do tej istniejącej pamięci jest bezpieczny. Pierwsze dopisanie alokuje pewną zapasową pojemność, a kolejne dopisania zapisują dane w jej obrębie. Wynik? Amortyzowane O(1) na każde dopisanie – identycznie jak działa std::vector w C++.

Gdzie jest haczyk? Jeśli Twój binarny „ucieknie” – co oznacza, że zostanie zapisany w słowniku procesu, zwrócony do wywołującego lub dopisany do czegoś innego – zabawa się kończy. Wracasz do kopiowania.

Ten wzorzec pojawia się wszędzie w obsłudze protokołów binarnych. Kiedy budujesz odpowiedzi DNS iteracyjnie, kluczowe jest utrzymanie ciasnego przepływu akumulatora.

Sub-binaria: darmowe wycinanie (w większości)

Dopasowywanie (matching) w binariach tworzy to, co BEAM nazywa sub-binariami – mikroskopijne termy, które wskazują na oryginalne dane bez ich kopiowania. Kiedy piszesz:

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

Żadne dane nie są kopiowane. Rest to po prostu „okno” do oryginalnego pakietu.

Pojawia się jednak subtelność, na której wykładają się ludzie: BEAM wewnętrznie utrzymuje kontekst dopasowania podczas dopasowywania rekurencyjnego, co w zasadzie jest kursorem przesuwającym się przez bajty. Nie stworzy sub-binarnego obiektu, dopóki faktycznie nie pozwolisz mu „uciec” – czyli nie zwrócisz go, nie zapiszesz lub nie przekażesz w miejsce, gdzie może zostać na dłużej.

Ma to ogromne znaczenie dla parsowania DNS. Każde zapytanie przychodzi jako binarny, jest dopasowywane do struktury protokołu i musi wygenerować odpowiedź. Zrozumienie, kiedy tworzysz nową pracę, a kiedy tylko przeglądasz dane, to różnica między parserem, który skaluje się, a takim, który dławi się pod obciążeniem.

Dlaczego reprezentacja liczb całkowitytych ma większe znaczenie, niż myślisz

W 64-bitowym BEAM liczby całkowite zajmują kilka bitów na swoją etykietę typu. Zostawia to 60 bitów na właściwą wartość. Liczby całkowite, które mieszczą się w tej przestrzeni, to „małe liczby całkowite” (small integers) – wartości natychmiastowe, które żyją bezpośrednio w rejestrach procesu, nigdy nie dotykając sterty (heap).

Wszystko, co większe, staje się „bignum”, co wymaga alokacji na stercie i uwzględnienia odśmiecania pamięci (garbage collection). Brzmi to ezoterycznie, ale staje się krytyczne, gdy zajmujesz się tego typu manipulacją bitami, jakiej wymaga praca z protokołami binarnymi.

Technika ta nazywa się SWAR (SIMD Within A Register) – pakowanie wielu wartości w jedną liczbę całkowitą, aby móc operować na nich równolegle na poziomie procesora. Dla DNS oznacza to parsowanie wielu bajtów danych w jednej operacji. Ale działa to tylko wtedy, gdy Twoje spakowane wartości mieszczą się w „idealnym zakresie” małych liczb całkowitych.

Kiedy się nie mieszczą, nie płacisz tylko kosztów alokacji pamięci – potencjalnie wyzwalasz cykle odśmiecania pamięci, które inaczej by nie istniały. Przy milionach zapytań na sekundę te cykle sumują się.

Co to oznacza dla Twojego kodu

Oto praktyczne wnioski: nie musisz pisać NIF-ów ani sięgać po C, aby uzyskać poważne zyski wydajnościowe. Musisz zrozumieć, jak BEAM myśli o danych.

Kilka wzorców wartych zapamiętania:

1. Utrzymuj akumulatory binarne w ryzach. Pętle typu „tylko dopisuj” (append-only) są Twoim przyjacielem. Gdy tylko binarny „ucieknie”, model kosztów zmienia się drastycznie.

2. Dopasowuj w miejscu, gdy to możliwe. Używaj dopasowania wzorców (pattern matching), aby przeglądać dane, a nie je kopiować. BEAM jest pod to zoptymalizowany.

3. Pilnuj rozmiaru liczb całkowitych. Podczas wykonywania operacji arytmetycznych lub manipulacji bitami, zastanów się, czy Twoje wartości pozostaną w zakresie „małych liczb całkowitych”.

4. Profiluj z myślą o redukcjach. BEAM planuje pracę w redukcjach (w przybliżeniu wywołaniach funkcji). Gorąca pętla, która wykonuje więcej dopasowań w mniejszej liczbie wywołań funkcji, wykonuje mniej pracy z perspektywy schedulera.

Dla każdego, kto buduje systemy obsługujące protokoły binarne – czy to DNS, ramki WebSocket, czy niestandardowe implementacje protokołów – te wzorce mają bezpośrednie zastosowanie. BEAM daje Ci niesamowite narzędzia do pisania szybkiego kodu, ale są one użyteczne tylko wtedy, gdy rozumiesz ten model.

Widzieliśmy zespoły, które sięgały po NIF-y lub NIF-y w Rust, podczas gdy głębsze zrozumienie semantyki BEAM dałoby im 90% zysków bez żadnej złożoności. Runtime jest mądrzejszy, niż często przypuszczamy – naszym zadaniem jest pisanie kodu, który pozwala mu się wykazać.

W NameOcean zawsze myślimy o wydajności w dużej skali. Niezależnie od tego, czy uruchamiasz Vibe Hosting dla swojego kolejnego projektu, czy zarządzasz DNS dla aplikacji o dużym natężeniu ruchu, zrozumienie tych fundamentów pomaga budować systemy, które pozostają szybkie, gdy robi się intensywnie.

Miłego kodowania i niech Twoje binarne zawsze dopisują się wydajnie!

Read in other languages:

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