Разкриване на тайните на BEAM бинарните файлове: Как направихме нашата DNS библиотека светкавично бърза (без NIF-ове)

Разкриване на тайните на BEAM бинарните файлове: Как направихме нашата DNS библиотека светкавично бърза (без NIF-ове)

Окт 20, 2026 beam elixir erlang dns performance optimization binary protocols backend development

Когато управлявате платформа, която се занимава с регистрация на домейни, DNS хостинг и облачна инфраструктура, както ние в NameOcean, производителността не е просто „приятно допълнение“ – тя е основата на надеждността. Ето защо се вълнуваме, когато видим умни истории за оптимизация от екосистемата Erlang, и защо искаме да разгледаме една от любимите ни за вас.

Наскоро попаднахме на завладяващ подробен анализ от екипа на DNSimple относно тяхната работа по оптимизация на dns_erlang – тяхната библиотека за DNS с отворен код. Резултатите им бяха поразителни: 12% по-малко използване на CPU на заявка и 14% повече обработени заявки в секунда – и всичко това само чрез чисти оптимизации на BEAM, без нужда от NIF (Native Implemented Functions). За всеки, който изгражда системи с висока пропускателна способност в Elixir или Erlang, това е задължително четене.

Нека разгледаме какво прави това възможно.

Менталната модел на бинарните данни в BEAM

Ето нещо, което изненадва много Elixir разработчици: добавянето (appending) към бинарни данни в тесен цикъл не е толкова скъпо, колкото може да мислите.

Когато напишете нещо като:

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

Може да очаквате това да е O(n²) – всяка итерация създава нов бинарен низ и копира всичко. Но BEAM е по-умен от това.

Тъй като бинарните данни са неизменни (immutable), компилаторът може да докаже, че ако acc е създаден току-що от предходно добавяне и нищо друго няма референция към него, писането директно в тази съществуваща памет е безопасно. Първото добавяне заделя малко допълнителна капацитет, а следващите добавят директно в нея. Резултатът? Амортизирано O(1) на добавяне – идентично с това, как работи std::vector в C++.

Уловката? Ако вашият бинарен низ „избяга“ (escape) – т.е. бъде съхранен в речника на процеса (process dictionary), върнат на извикващия го процес или добавен към нещо друго – партито приключва. Връщате се към копирането.

Този модел се среща навсякъде при обработката на бинарни протоколи. Когато изграждате DNS отговори итеративно, поддържането на този поток на акумулатор е изключително важно.

Суб-бинарни данни: Безплатно рязане (почти)

Съпоставянето (matching) на бинарни данни създава това, което BEAM нарича суб-бинарни данни – малки термини, които сочат към оригиналните данни, без да ги копират. Когато напишете:

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

Данните не се копират. Rest е просто прозорец към оригиналния пакет.

Но тук е деликатността, която обърква хората: BEAM поддържа вътрешен контекст на съпоставяне, когато правите рекурсивно съпоставяне – по същество курсор, който върви през байтовете. Той няма да материализира суб-бинарни данни, освен ако реално не позволите на тях да „избягат“ – като ги върнете, съхраните или предадете на някъде, където ще останат.

Това е от огромно значение за DNS парсването. Всяка заявка пристига като бинарен низ, съпоставя се със структурата на протокола и трябва да генерира отговор. Разбирането на това кога създавате нова памет и кога просто разглеждате съществуващи данни е разликата между парсер, който мащабира, и такъв, който се „задушава“ под натоварване.

Защо представянето на целите е по-важно, отколкото мислите

При 64-битова BEAM, целите заемат няколко бита за своя тип таг. Това оставя 60 бита за самата стойност. Целите, които се събират в това пространство, са „малки цели“ (small integers) – непосредствени стойности, които живеят директно в регистрите на процеса, без никога да докосват хипа (heap).

Всичко по-голямо става „голямо число“ (bignum), което изисква заделяне на памет в хипа и управление на отпадъчната памет (garbage collection). Звучи езотерично, но става критично, когато правите вида манипулации с битове, които бинарната работа изисква.

Техниката се нарича SWAR (SIMD Within A Register) – пакетиране на множество стойности в един единствен цяло число, така че да можете да работите с тях паралелно на ниво процесор. За DNS това означава парсване на много байтове данни в всяка операция. Но това работи само ако вашите пакетирани стойности се събират в „златната среда“ на малките цели.

Когато не се събират, вие не плащате само разходите за заделяне на памет – вие потенциално задействате цикли на събиране на отпадъчна памет, които иначе не биха съществували. При милиони заявки в секунда, тези цикли се натрупват.

Какво означава това за вашия код

Ето практическия извод: не е необходимо да пишете NIF или да се обръщате към C, за да постигнете сериозни подобрения в производителността. Трябва да разберете как BEAM мисли за данните.

Няколко модела, които си струва да усвоите:

1. Поддържайте бинарните акумулатори компактни. Циклите, които само добавят (append-only), са ваши приятели. След като един бинарен низ „избяга“, моделът на разходите се променя драстично.

2. Съпоставяйте на място, когато е възможно. Използвайте pattern matching, за да разглеждате данните, вместо да ги копирате. BEAM е оптимизиран за това.

3. Следете размерите на целите. При извършване на аритметика или битова манипулация, мислете дали вашите стойности ще останат в диапазона на „малките цели“.

4. Профилирайте с предвид на редукциите (reductions). BEAM планира работата чрез редукции (приблизително функционални повиквания). Цикъл, който прави повече съпоставяния с по-малко повиквания на функции, извършва по-малко работа от гледна точка на планировчика.

За всеки, който изгражда системи, обработващи бинарни протоколи – било то DNS, WebSocket кадри или персонализирани протоколи – тези модели се прилагат директно. BEAM ви дава невероятни инструменти за писане на бърз код, но те са полезни само ако разбирате модела.

Виждали сме екипи да използват NIF или Rust NIF, когато по-дълбоко разбиране на семантиката на BEAM би им осигурило 90% от печалбите без никаква сложност. Runtime средата е по-умна, отколкото често смятаме – нашата задача е да пишем код, който ѝ позволява да се представи.

В NameOcean винаги мислим за производителността при мащаб. Независимо дали стартирате Vibe Hosting за следващия си проект или управлявате DNS за приложение с висок трафик, разбирането на тези основи ви помага да изграждате системи, които остават бързи, когато натоварването се увеличи.

Приятно програмиране и нека вашите бинарни данни винаги се добавят ефективно!

Read in other languages:

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