<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Андрей Жаркевич</title><generator>teletype.in</generator><description><![CDATA[Пишу статьи, готовлю обзоры новостей и аналитические исследования. Специализируюсь на ИТ и ИБ, финансах и инвестициях. Увлекаюсь ЗОЖ и фитнесом.]]></description><image><url>https://img2.teletype.in/files/11/64/11645c69-6688-42cb-b106-0bbef324335d.jpeg</url><title>Андрей Жаркевич</title><link>https://azhark.cc/</link></image><link>https://azhark.cc/?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/azhark?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/azhark?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Tue, 21 Apr 2026 10:57:58 GMT</pubDate><lastBuildDate>Tue, 21 Apr 2026 10:57:58 GMT</lastBuildDate><item><guid isPermaLink="true">https://azhark.cc/hash-tables</guid><link>https://azhark.cc/hash-tables?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/hash-tables?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Хеш-таблицы: структура данных, которая изменила программирование</title><pubDate>Mon, 20 Apr 2026 19:58:24 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/62/6d/626df89b-83d8-465e-bc57-4ef40fa532f6.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/ea/14/ea142a8e-a238-4cac-ba11-8a3d4651ae13.png"></img>Представьте файл на восемь мегабайт. Внутри — одна очень длинная строка: aa=1&amp;bb=2&amp;cc=3&amp;… и так несколько сотен тысяч раз. Ничего особенного — обычное тело POST-запроса, какие веб-сервер разбирает тысячами в секунду. Кроме одной детали: имена параметров подобраны с точностью криптоаналитика так, чтобы все до единого давали одинаковый хеш.]]></description><content:encoded><![CDATA[
  <figure id="3rgg" class="m_original">
    <img src="https://img3.teletype.in/files/ea/14/ea142a8e-a238-4cac-ba11-8a3d4651ae13.png" width="1536" />
  </figure>
  <p id="OTg5">Представьте файл на восемь мегабайт. Внутри — одна очень длинная строка: <code>aa=1&amp;bb=2&amp;cc=3&amp;…</code> и так несколько сотен тысяч раз. Ничего особенного — обычное тело POST-запроса, какие веб-сервер разбирает тысячами в секунду. Кроме одной детали: имена параметров подобраны с точностью криптоаналитика так, чтобы все до единого давали одинаковый хеш.</p>
  <p id="YY8H">Отправьте этот файл на сервер PHP образца 2011 года — и получите сервер, занятый примерно на час. Один запрос. Один процессор. Сто процентов загрузки. Замените PHP на Java, Python, Ruby, ASP.NET или Node.js — ничего не изменится: та же сотка, тот же час. Мегабитный канал превращается в инструмент, способный положить целый кластер, — без ботнета, без уязвимости в приложении, без флуда. Просто очень тщательно сделанная форма.</p>
  <p id="RYHT">Ровно это и демонстрировали Александр Клинк и Юлиан Велде 28 декабря 2011 года на конференции 28C3 — в докладе с невинным названием «Эффективный отказ в обслуживании веб-платформ». К следующему утру появились первые записи CVE: CVE-2011-4885 — PHP, CVE-2011-4858 — Tomcat, и дальше — целая серия для остальных. Неделю новостных лент спустя отрасль уже переписывала реализации хеш-таблиц.</p>
  <p id="WeDf">Хуже всего было то, что атаку предсказали восемь лет назад — и все пропустили предсказание.</p>
  <h2 id="предсказание-которое-никто-не-услышал">Предсказание, которое никто не услышал</h2>
  <p id="LRKc">В 2003 году двое исследователей из Университета Райса, Скотт Кросби и Дэн Уоллач, опубликовали статью с сухим академическим названием — <em>«Denial of Service via Algorithmic Complexity Attacks»</em>. В переводе: «Отказ в обслуживании через атаки на вычислительную сложность».</p>
  <p id="IHXL">Кросби и Уоллач описали сценарий с хирургической точностью: если протокол принимает пользовательские данные в хеш-таблицу, а хеш-функция предсказуема, атакующий может заранее вычислить ключи-коллизии. Таблица, которая должна работать за миллисекунды, деградирует до связного списка. В статье авторы показали, как с помощью этой техники можно положить Perl, Python-веб-приложения и сетевое устройство Snort.</p>
  <p id="QMIi">Статья получила вежливые отзывы, несколько ссылок в академических кругах и легла на полку. Разработчики Perl пофиксили свою хеш-функцию — единственные из всей индустрии. Остальные сочли атаку «теоретической».</p>
  <p id="HwNc">Понадобилось восемь лет и одна публичная демонстрация на 28C3, чтобы все языки программирования массово начали переписывать реализации своих хеш-таблиц.</p>
  <p id="uhX1">Чтобы понять, почему одна демонстрация смогла поставить в неудобную позу половину отрасли, нужно сначала разобраться, что такое хеш-таблица и почему она есть буквально везде.</p>
  <h2 id="от-картотеки-к-массиву-как-всё-начиналось">От картотеки к массиву: как всё начиналось</h2>
  <p id="H2qC">Январь 1953 года. Ханс Петер Лун, инженер IBM, трудится над задачей, которая сегодня кажется смешной: нужно быстро найти карточку в картотеке из тысяч записей. Компьютеры тогда занимали комнаты, а «память» измерялась не в гигабайтах, а в магнитных барабанах.</p>
  <p id="SB6G">Лун рассуждает как инженер. Если у нас есть полка на сто ячеек, и мы кладём каждую карточку наугад, поиск в среднем потребует просмотра половины полки. Но что если от самой карточки вычислить число, а потом положить её в ячейку с этим номером? Тогда и искать можно в одно действие: вычислить число от запроса и сразу прыгнуть к ячейке.</p>
  <p id="PmCY">Это и был прообраз хеш-таблицы. Внутренний документ IBM, в котором Лун её описал, никогда не публиковался в открытом доступе. Мир узнал о приёме позже, из работы Арнольда Думи, Джина Амдала и других. Но первым был именно Лун.</p>
  <p id="5nv7">Смысл приёма простой до неприличия:</p>
  <ol id="s0y0">
    <li id="nKyW">Есть массив, скажем, на 16 ячеек.</li>
    <li id="k22l">От ключа («Иванов», 42, адрес страницы) вычисляем <em>хеш</em> — число, которое должно равномерно размазывать разные ключи по всему диапазону.</li>
    <li id="1ANY">Берём остаток от деления хеша на 16, получаем индекс ячейки.</li>
    <li id="isem">Кладём туда значение.</li>
  </ol>
  <p id="DENP">Поиск работает так же: вычислил хеш, взял остаток, прыгнул в ячейку. Одно действие. То самое O(1), за которое хеш-таблицу любят все.</p>
  <h2 id="когда-два-ключа-хотят-в-одну-ячейку">Когда два ключа хотят в одну ячейку</h2>
  <p id="SYzi">А теперь представьте: у нас 16 ячеек, а ключей мы вставляем двадцать. По принципу Дирихле хотя бы двум ключам обязательно достанется один и тот же индекс. Это называется коллизией, и с ней надо что-то делать.</p>
  <p id="YDM0">Есть два принципиально разных подхода.</p>
  <p id="QLXj"><strong>Цепочки.</strong> В каждой ячейке живёт не значение, а связный список. Столкнулись двое — добавили к списку. Просто, надёжно, но список разбросан по памяти мелкими кусочками. Процессор ненавидит такую раскладку: каждый переход по указателю — потенциальный промах кеша, то есть сотня тактов ожидания оперативной памяти.</p>
  <p id="JYc9"><strong>Открытая адресация.</strong> Ячейка занята — идём к следующей. Этот подход похож на парковку у торгового центра: если твоё место занято, ищешь ближайшее свободное. Данные лежат подряд, процессор доволен. Но удаление превращается в головоломку: если просто очистить ячейку, поиск сломается. Он остановится на «дырке», не дойдя до нужного ключа. Приходится помечать ячейки как «удалённые, но не пустые».</p>
  <p id="QaCj">Оба подхода работают идеально <em>в среднем</em>. Проблема начинается, когда кто-то специально подбирает ключи, которые сталкиваются.</p>
  <h2 id="когда-o1-превращается-в-on">Когда O(1) превращается в O(n)</h2>
  <p id="KBZB">Вернёмся к докладу на 28C3. Вся красота атаки именно в этом превращении.</p>
  <p id="GXfN">Пусть атакующий знает: веб-сервер парсит параметры запроса <code>foo=1&amp;bar=2&amp;baz=3</code> в хеш-таблицу, а хеш-функция — детерминированная и известная (в 2011 году всё это было открыто задокументировано). Тогда он заранее, на своём ноутбуке, подбирает тысячу имён параметров, которые все хешируются в одно и то же число.</p>
  <p id="0NHJ">Сервер получает безобидный на вид POST-запрос. Начинает вставлять параметры. Первый — O(1). Второй — O(1), но таблица уже знает: в этой ячейке коллизия, надо сравнивать. Третий — уже сравнивается с двумя. К тысячному параметру каждая вставка — это обход тысячи коллизий. Суммарно — O(n²), где n — количество параметров.</p>
  <p id="oQIB">На демонстрации Клинк и Велде показали: POST-запрос в 8 МБ занимал сервер на PHP примерно на час. Один запрос. Один мегабитный канал — и можно положить целый кластер. Ни флуда, ни ботнета, ни уязвимости в приложении. Просто очень тщательно составленный запрос.</p>
  <h2 id="ответ-siphash-и-секретный-ключ">Ответ: SipHash и секретный ключ</h2>
  <p id="Ibm8">Инженерное сообщество отреагировало быстро и в три голоса.</p>
  <p id="mQNT">Во-первых — лимиты. PHP добавил директиву <code>max_input_vars</code>. Tomcat, Jetty и другие серверы приложений ввели аналогичные ограничения. Это костыль, но рабочий: атакующий не может отправить миллион параметров, если сервер принимает максимум тысячу.</p>
  <p id="NkCq">Во-вторых — рандомизация. Если хеш-функция зависит от <em>секретного числа</em>, которое выбирается при запуске программы, предсказать коллизии заранее невозможно. Атакующий больше не может на своём ноутбуке подобрать «ядовитые» ключи. Для этого ему нужно как-то добыть сам секрет из работающего сервера.</p>
  <p id="x5xu">В-третьих — криптография. В 2012 году Жан-Филипп Омассон и Даниэль Бернштейн опубликовали <em>SipHash</em> — хеш-функцию, специально спроектированную для защиты хеш-таблиц. Это не классическая криптографическая функция вроде SHA-256. Для хеширования паролей она не годится. Но у неё есть ключевое свойство: без знания секретного ключа подобрать коллизии математически невозможно (точнее, эквивалентно взлому стойкого шифра).</p>
  <p id="fZi7">SipHash стал стандартом де-факто. Python перешёл на него в версии 3.4. Ruby, Perl, Rust, Haskell — все последовали примеру. Это редкий случай, когда академическая статья за несколько лет стала промышленным стандартом.</p>
  <h2 id="робин-гуд-против-богачей">Робин Гуд против богачей</h2>
  <p id="I1IC">Пока одни исследователи разбирались с безопасностью, другие воевали за скорость.</p>
  <p id="lxnw">В 1986 году канадский аспирант Педро Селис защитил диссертацию с задорным названием <em>Robin Hood Hashing</em> — «Хеширование Робин Гуда». Идея простая и очень красивая.</p>
  <p id="sOxs">Когда мы вставляем ключ при открытой адресации и натыкаемся на занятую ячейку, мы идём дальше. Каждый такой шаг — это «бедность»: чем дальше элемент от своей идеальной позиции, тем больше он страдает при каждом поиске. Обычная открытая адресация несправедлива: «богачи» (те, кто попал на своё место с первого раза) живут спокойно, а «бедняки» обречены на длинные поиски.</p>
  <p id="Wdjq">Селис предложил: при вставке сравнивать, кто больше «настрадался». Если новый ключ уже прошёл дальше, чем ключ в текущей ячейке, они меняются местами. Новый ключ занимает ячейку, а «богач» идёт искать место дальше. Забираем у тех, кому повезло, отдаём тем, кому нет, отсюда и название.</p>
  <p id="duIB">Результат выглядит скромно, но на деле впечатляет. В обычной открытой адресации при заполнении 90 % дисперсия длины поиска — 16,2 шага. В варианте Робин Гуда — 0,98 шага. То есть поиски становятся не просто быстрыми в среднем, а одинаково быстрыми. Хуже почти никому не будет.</p>
  <p id="y6Fa">Rust с самого начала и до 2019 года использовал именно Робин Гуда. Это был хороший выбор до тех пор, пока не появился следующий.</p>
  <h2 id="swiss-tables-как-16-сравнений-стали-одним">Swiss Tables: как 16 сравнений стали одним</h2>
  <p id="BGht">В 2017 году на конференции CppCon инженер Google Мэтт Кулукундис вышел на сцену с докладом «Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step». За 45 минут он рассказал, как команда Google — сам Кулукундис, Сэм Бензакен, Алкис Эвлогименос и Роман Перепелица — переписала хеш-таблицу для внутренних сервисов компании. Результат назвали <em>Swiss Table</em> (по аналогии со швейцарским сыром: много аккуратных «дырок» — свободных ячеек).</p>
  <p id="sjzy">Ключ к скорости — инструкции SIMD (<em>Single Instruction, Multiple Data</em>, «одна инструкция на много данных»). Идея такая:</p>
  <ol id="Zstd">
    <li id="7whi">Хеш ключа делится на две части: длинную (57 бит) и короткую (7 бит). Длинная часть определяет, в какой <em>группе</em> из 16 ячеек искать. Короткая — «отпечаток», который хранится отдельно, в массиве метаданных по одному байту на ячейку.</li>
    <li id="QKAS">При поиске процессор одной инструкцией загружает 16 байт метаданных в 128-битный регистр. Ещё одной инструкцией (<code>_mm_cmpeq_epi8</code>) сравнивает отпечаток искомого ключа сразу со всеми 16 ячейками. Результат сравнения — битовая маска: где совпало, там и смотрим реальный ключ.</li>
  </ol>
  <p id="EtOT">По сути, шестнадцать шагов поиска за одну машинную команду. На реальных нагрузках Google прирост оказался двукратным, и это при том, что прежняя реализация уже была очень хорошей.</p>
  <p id="JO4G">История на этом не кончается. Когда Swiss Table открыли как часть библиотеки <em>Abseil</em> в 2018 году, другие языки потянулись следом. Rust в 2019-м заменил Робин Гуда на библиотеку <code>hashbrown</code> — порт Swiss Table. Go в версии 1.24 (февраль 2025) выбросил свою схему с цепочками по 8 элементов и переехал на Swiss Table — карты стали до 60 % быстрее на типичных операциях.</p>
  <h2 id="go-и-сознательный-хаос-история-рандомизации">Go и сознательный хаос: история рандомизации</h2>
  <p id="kJZH">Из всей этой саги особняком стоит решение, принятое командой Go ещё в 2012 году, и это одна из лучших историй о том, как проектировать язык.</p>
  <p id="5Xcl">До Go 1.0 порядок обхода <code>map</code> через <code>range</code> был детерминированным. Ну, или почти — он зависел от деталей реализации. Программисты быстро это обнаружили и начали писать код, зависящий от порядка: тесты, которые сравнивали сериализованные строки, кеши, где порядок влиял на результат, логи, где хотелось «одинакового вывода».</p>
  <p id="mOLu">Команда Go поняла: если оставить как есть, любое будущее изменение внутренней реализации сломает чужой код. И приняла смелое решение — специально внести случайность. При каждом вызове <code>range</code> стартовая ячейка выбирается случайно через быстрый генератор <code>fastrand()</code>. Два цикла <code>range</code> подряд по одной и той же карте дадут разный порядок.</p>
  <p id="qAns">Это не защита от атак. Это защита от себя — от программистов, которые случайно (или специально) начнут зависеть от порядка и превратят деталь реализации в часть контракта. Когда в Go 1.24 заменили всю реализацию хеш-таблицы целиком, ни одна программа не сломалась, потому что зависеть было не от чего.</p>
  <p id="H6Ut">Мораль простая: если деталь реализации не должна быть частью контракта, надёжнее всего сделать её <em>явно случайной</em>. Иначе её неизбежно начнут использовать, и однажды это создаст проблемы.</p>
  <p id="phlP">Теперь, когда история понятна, посмотрим на то, что лежит в «коробке» у современных языков.</p>
  <h2 id="хеш-таблицы-в-четырёх-языках">Хеш-таблицы в четырёх языках</h2>
  <p id="pX8n"><strong>Rust.</strong> Стандартный <code>HashMap</code> — это библиотека <code>hashbrown</code>, порт Swiss Table, с хеш-функцией SipHash-1-3. Каждый запуск программы выбирает новый секретный ключ. Попытка отсортировать содержимое карты по ключам требует явного сбора в <code>Vec</code> — <code>HashMap</code> принципиально не реализует упорядоченность.</p>
  <p id="jIQp"><strong>Go.</strong> После 1.24 — Swiss Table. Хеш-функция из пакета <code>hash/maphash</code> получает собственный секретный ключ при каждом создании карты. Сравнить две карты через <code>==</code> компилятор не даст.</p>
  <p id="LDPh"><strong>Zig.</strong> Есть <code>StringHashMap</code> для строковых ключей и <code>AutoHashMap</code> для типов, у которых хеш можно вывести автоматически. Аллокатор передаётся явно. Можно использовать арену для временных таблиц, что в «больших» языках недоступно. По умолчанию используется Wyhash — быстрая, но некриптографическая функция. Встроенной рандомизации секретного ключа нет. Это решение пользователь принимает сам.</p>
  <p id="v10H"><strong>Nim.</strong> В стандартной библиотеке имеется модуль <code>std/tables</code> с типами <code>Table[K, V]</code> (неупорядоченная) и <code>OrderedTable[K, V]</code> (сохраняет порядок вставки; в Go и Rust отдельного типа нет — его пришлось бы собирать руками). Хеш-функция берётся из <code>std/hashes</code>, стандартная реализация для строк детерминирована между запусками. Рандомизация секретного ключа — задача разработчика: либо подмешивать собственный seed, либо подключать сторонний SipHash.</p>
  <p id="Svr0">Примеры кода:</p>
  <ul id="dYG0">
    <li id="vOl3"><a href="https://zharkevich.ru/blog/2026/hash-tables/hash_demo.rs" target="_blank">hash_demo.rs</a> — Rust;</li>
    <li id="63e7"><a href="https://zharkevich.ru/blog/2026/hash-tables/hash_demo.go" target="_blank">hash_demo.go</a> — Go;</li>
    <li id="qNdF"><a href="https://zharkevich.ru/blog/2026/hash-tables/hash_demo.zig" target="_blank">hash_demo.zig</a> — Zig;</li>
    <li id="WKIQ"><a href="https://zharkevich.ru/blog/2026/hash-tables/hash_demo.nim" target="_blank">hash_demo.nim</a> — Nim.</li>
  </ul>
  <h2 id="история-повторяется-свежие-уязвимости">История повторяется: свежие уязвимости</h2>
  <p id="3bT1">Мы говорим о 2011 годе как о далёком прошлом, но класс уязвимостей никуда не делся. Стоит разработчику забыть о рандомизации, как проблема возвращается:</p>
  <ul id="eaoO">
    <li id="ahrs">CVE-2024-21538 в пакете <code>cross-spawn</code> — предсказуемый разбор путей приводит к квадратичному времени;</li>
    <li id="NH9H">CVE-2024-4067 в <code>micromatch</code> — коллизии в разборе шаблонов для поиска файлов;</li>
    <li id="XkZ8">CVE-2024-21490 в Angular — регулярные выражения плюс хеш-таблица.</li>
  </ul>
  <p id="AeLm">Общее правило простое. Любая система, которая принимает пользовательский ввод и кладёт его в структуру данных, чувствительную к распределению ключей, потенциально уязвима. Если хеш-функция не рандомизирована — уязвима гарантированно.</p>
  <h2 id="что-осталось-за-кадром">Что осталось за кадром</h2>
  <p id="1Jk6">Хеш-таблица — одна из тех структур, без которых современное программирование невозможно представить. <code>dict</code> в Python, объект в JavaScript, <code>map</code> в Go, <code>HashMap</code> в Rust, <code>Table</code> в Nim — под всеми этими разными именами работает одна и та же идея Ханса Петера Луна, которой скоро исполнится 75 лет.</p>
  <p id="qtQo">Но за уютным фасадом O(1) скрывается долгая инженерная драма: выбор функции, борьба с коллизиями, защита от атак, компромисс между скоростью и предсказуемостью. Swiss Table с SIMD-пробированием — вершина эволюции на сегодня. Если история чему-то учит, через пять-десять лет появится следующая, и мы снова будем переписывать стандартные библиотеки.</p>
  <p id="0wjj">А самое важное — помнить, что O(1) существует только на бумаге. В реальности всегда есть ключи, которые сделают ваш сервер очень, очень медленным. Вопрос только в том, догадается ли об этом атакующий раньше вас.</p>
  <h2 id="источники">Источники</h2>
  <ul id="eYEa">
    <li id="lOYy"><a href="https://www.cs.rice.edu/~scrosby/hash/CrosbyWallach_UsenixSec2003.pdf" target="_blank">Denial of Service via Algorithmic Complexity Attacks (Crosby, Wallach, 2003)</a> — предсказание HashDoS за восемь лет до атаки;</li>
    <li id="jzfi"><a href="https://events.ccc.de/congress/2011/Fahrplan/events/4680.en.html" target="_blank">Effective DoS on Web Application Platforms (Klink, Wälde, 28C3, 2011)</a> — доклад, перевернувший веб-разработку;</li>
    <li id="Iimo"><a href="https://cr.yp.to/siphash/siphash-20120918.pdf" target="_blank">SipHash: a fast short-input PRF (Aumasson, Bernstein, 2012)</a> — спецификация функции, защитившей отрасль;</li>
    <li id="xeLy"><a href="https://abseil.io/about/design/swisstables" target="_blank">Swiss Tables Design Notes (Abseil)</a> — описание Swiss Table от Google;</li>
    <li id="2Hdi"><a href="https://go.dev/blog/swisstable" target="_blank">Faster Go maps with Swiss Tables (Go Blog, 2025)</a> — переход Go на Swiss Table;</li>
    <li id="gV5y"><a href="https://github.com/rust-lang/hashbrown" target="_blank">hashbrown — Rust HashMap implementation</a> — реализация Swiss Table для Rust;</li>
    <li id="qf7h"><a href="https://www.sebastiansylvan.com/post/robin-hood-hashing-should-be-your-default-hash-table-implementation/" target="_blank">Robin Hood Hashing (Sebastian Sylvan)</a> — объяснение алгоритма Селиса;</li>
    <li id="WNfp"><a href="https://github.com/golang/go/issues/6719" target="_blank">Go Map Iteration Order Randomization (Issue #6719)</a> — история рандомизации порядка обхода в Go.</li>
  </ul>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/a-wormy-apple</guid><link>https://azhark.cc/a-wormy-apple?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/a-wormy-apple?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Червивое яблоко</title><pubDate>Sun, 19 Apr 2026 13:05:08 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/fb/1e/fb1e96e0-c161-49f8-a566-8b4399b2a9bd.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/64/5b/645b5536-111d-40c8-b032-76bedec3b2e5.png"></img>Ирвин никак не мог понять, что ему не нравится в строчках выполненного домашнего задания по программированию. Среда разработки iBasic радостно подмигивала зелёным: ошибок в коде не было. Но мальчику всё равно казалось, что чего-то здесь не хватает. Изящества, выразительности, лаконичности, — чего-то неуловимого, чего-то, что он, ученик восьмого класса лицея имени Стива Джобса не мог сформулировать. Ирвин привычным жестом закрыл среду iBasic, щёлкнул по увесистому кубику учебника истории и приступил к изучению параграфа, заданного на дом. Перед глазами появилась реконструкция Ледового побоища из исторического фильма. Крики, стоны и звон оружия заполнили крошечную комнату. Социальное жильё никогда не бывает просторным.]]></description><content:encoded><![CDATA[
  <figure id="TYsQ" class="m_original">
    <img src="https://img3.teletype.in/files/64/5b/645b5536-111d-40c8-b032-76bedec3b2e5.png" width="1672" />
  </figure>
  <p id="nxE5">Ирвин никак не мог понять, что ему не нравится в строчках выполненного домашнего задания по программированию. Среда разработки iBasic радостно подмигивала зелёным: ошибок в коде не было. Но мальчику всё равно казалось, что чего-то здесь не хватает. Изящества, выразительности, лаконичности, — чего-то неуловимого, чего-то, что он, ученик восьмого класса лицея имени Стива Джобса не мог сформулировать. Ирвин привычным жестом закрыл среду iBasic, щёлкнул по увесистому кубику учебника истории и приступил к изучению параграфа, заданного на дом. Перед глазами появилась реконструкция Ледового побоища из исторического фильма. Крики, стоны и звон оружия заполнили крошечную комнату. Социальное жильё никогда не бывает просторным.</p>
  <p id="x6LQ">Ирвин остановил фильм. В воздухе над столом замер воин в доспехах с гримасой боли на окровавленном лице. Неудовлетворённость, оставшаяся после задания по программированию, мешала сосредоточиться на истории. К тому же он вспомнил, как смотрел этот фильм в прошлый раз…</p>
  <hr />
  <p id="QjlF">В тот день его отца арестовали. Они вместе смотрели фильм о битве на Чудском озере, только не на маленьком голоэкранчике школьного планоута, а на шикарном голопроекторе, который отец купил на новогодней распродаже. Ирвин восхищённо наблюдал, как бородатые русичи громят тевтонских рыцарей на льду озера. В самый напряжённый момент фильм прервался. Комната наполнилась фиолетовым светом от появившегося в центре голографической проекции человека в судейской мантии.</p>
  <p id="x6WH">— Мартин Купер, вы нарушили имущественные права корпорации АйГейпл, повторно просмотрев фильм «Ледовое побоище», — прогремел усиленный мощной аудиосистемой голос. — Для совершения преступления вы использовали запрещённые законом хакерские инструментальные средства, что существенно отягощает вашу вину. За это в соответствии с законом вы приговариваетесь к тюремному заключению сроком на пятнадцать лет. Ваше имущество будет конфисковано для компенсации нанесённого ущерба. Ваш сын Ирвин Купер переходит под опеку корпорации, ему выделят социальное жильё в квартале имени Марка Збиковски. Приговор окончательный, аппеляция не предусмотрена. У вас есть пять минут, чтобы попрощаться с сыном.</p>
  <p id="0CZJ">От грохота голоса Ирвин задрожал. Смысл слов судьи с трудом доходил до сознания. Отец обнял его, и погладил по голове. А потом отстранился и глядя прямо в глаза, произнёс: «Ирвин, ничего не бойся! Учись как следует и живи дальше. Со мной всё будет в порядке. И не верь ничему, что будут говорить и писать обо мне!» Потом он вытер слёзы, катившиеся из глаз Ирвина, обнял его ещё раз и повернулся к неподвижной фигуре судьи: «Я готов!»</p>
  <p id="BDlx">Судья махнул рукой, раздался щелчок смарт-замка, и дверь в квартиру Куперов открылась. На пороге стояли трое мужчин в форме с нашивками в виде надкушенного яблока с двумя перекрещенными поверх него мечами.</p>
  <p id="M3IH">— Копирайт-полиция АйГейпл. Мартин Купер, следуйте за нами! — скомандовал черноволосый здоровяк.</p>
  <p id="iCzh">Отец спокойно пошёл к выходу. У Ирвина внутри всё опустилось.</p>
  <p id="Yvuj">— Папа! — рванулся он следом, но копирайт-полицейский его остановил.</p>
  <p id="HmNu">— Не торопись. Тебе не по пути с твоим отцом. Для тебя жизнь приготовила другое кино.</p>
  <p id="CCiV">Ирвин пытался вырваться, но силы были неравны. Трепыхаясь в крепких руках стража порядка, он успел заметить странный жест, который сделал отец, выходя из квартиры — собрал пальцы в щепоть, нарисовал ими в воздухе тройной круг и ткнул в его середину.</p>
  <hr />
  <p id="lXgU">— Заходи, Ирвин, это твой новый дом. Не хоромы, конечно, но жить вполне можно. Тебе повезло, ты будешь жить здесь один, а некоторые твои соседи делят такую комнату на троих, а то и на четверых.</p>
  <p id="57Sb">Ирвин слушал мягкий голос социального работника и пытался рассмотреть комнату, в которую его привели. Крохотное окно-иллюминатор под потолком, тусклое светодиодное освещение, серые стены, пластиковый пол, откидной стол, откидная кровать, умывальник. Он повернулся к своему сопровождающему.</p>
  <p id="Yl80">— Мистер Кук, а где здесь…</p>
  <p id="QCb4">— Туалет и душ в коридоре общего пользования. Для этой категории жилья индивидуальные санузлы не предусмотрены.</p>
  <p id="GpLH">Ирвин тяжело вздохнул. Сказать, что комната была тесной — это не сказать ничего. Ему казалось, что стены сближаются и пытаются его раздавить. Он сглотнул появившийся в горле комок.</p>
  <p id="nVys">— Ничего, ты привыкнешь. Все привыкают. Еду будешь получать в автомате рядом с лифтом. Нажимаешь кнопку, автомат считывает твой идентификатор со скинекта и выдаёт порцию горячей еды. Не вздумай хитрить, добавки здесь никому не полагается. Завтрак можно получить с шести до семи утра, ужин — с шести до восьми вечера. Не возьмёшь еду в это время — останешься голодным. Обедать будешь в школе. Разносолов не жди, всё очень просто и питательно. Деньги на проезд в школу и обратно будут ежемесячно зачисляться на твой личный счёт. Потратить их на что-то другое ты не сможешь. Если у тебя возникнут какие-то вопросы — мои контактные данные в твоём скинекте. Осваивайся, знакомься с соседями. Они тебе расскажут, что здесь и как. Удачи! И постарайся не повторять ошибок своего отца!</p>
  <p id="sOH0">Мистер Кук повернулся и ушёл. Ирвин остался один. Дверь со щелчком закрылась, и этот звук показался ему оглушительным. Только сейчас он окончательно осознал бесповоротность произошедшего. Чтобы не разреветься, мальчик принялся осматривать новое жилище.</p>
  <p id="wurd">Откидная кровать была также шкафом для одежды. Взять из него одежду или сложить её можно было лишь сложив кровать. Стол — рабочий и обеденный — компактный и многофункциональный, как и всё в этой комнате. С одной стороны стола выдвижной ящик с посудой и столовыми приборами, с другой — такого же размера ящик с беспроводной зарядкой для планоута.</p>
  <p id="xTT4">Ирвин снял рюкзак с личными вещами, достал планоут, включил. «Подключено к сети MZ» — бодро отрапортовал планоут. «Вам сообщение от социальной службы. Прочитать сейчас?» Ирвин сделал подтверждающий жест — поднял большой палец. В воздухе над планоутом появился прямоугольник информационного видеописьма.</p>
  <p id="qCZe">Сначала появилась заставка с эмблемой социальной службы АйГейпл. Затем пространство голопроекции заполнила фигура полноватого мужчины в очках. Он посмотрел прямо на Ирвина, кашлянул и заговорил.</p>
  <p id="xyo1">— От имени социальной службы корпорации АйГейпл приветствую вас в квартале имени Марка Збиковски. Это жильё сделано по специальному проекту корпорации под названием «Технологии против нищеты». С гордостью могу констатировать, что проект оказался весьма успешным, и на территориях, управляемых корпорацией, нищета как явление полностью отсутствует.</p>
  <p id="TIca">Мы верим, что все граждане должны иметь право получать всю необходимую информацию и поэтому сделали доступ к ней бесплатным для всех. Для этого каждый человек с самого рождения получает скинект — специальное устройство в виде браслета, которое безоперационным способом сращивается с левым запястьем и становится неотъемлемой частью своего владельца.</p>
  <p id="ZSm1">Скинект — уникальный идентификатор гражданина, заменивший все прочие виды удостоверений личности. Это кошелёк, медицинский ассистент и персональный цифровой помощник. С помощью скинекта граждане получают доступ к сети Айнет, созданной усилиями нашей замечательной корпорации. В отличие от устаревшего Интернета Айнет совершенно безопасна, в ней нет места вирусам, воровству и пиратству. Корпорация АйГейпл разработала технологии, которые защищают граждан от всех угроз, связанных с работой в сети. Если вы получили сообщение, то можете не сомневаться, что его прислал вам именно тот человек, который значится в поле «Отправитель». Благодаря обязательной идентификации нам даже удалось победить спамеров, от которых во времена Интернета не было спасения.</p>
  <p id="zKvX">С помощью скинекта вы можете транслировать изображение и звук на любое устройство, например, на планоут или на голопроектор. Управлять работой скинекта можно с помощью пульта медиаустройства, с клавиатуры планоута или его сенсорного экрана, с помощью жестов, которые замечательно распознаются встроенными в скинект датчиками положения в пространстве, или с помощью голоса. Благодаря скинекту вы никогда не заблудитесь, ведь в нём есть чипы для всех систем спутниковой навигации. Для работы скинект использует тепло вашего тела, поэтому вам никогда не придётся думать о замене батареек.</p>
  <p id="XHyE">Реализуя стратегию «Own different» («Владей иначе»), корпорация АйГейпл предоставила всем без исключения доступ к богатству цифрового контента. Каждый может смотреть фильмы, слушать музыку и читать книги. Для этого мы предоставляем базовый информационный пакет абсолютно бесплатно. В него включены лучшие произведения, созданные человечеством за много лет, а также свободный доступ ко всем публичным ресурсам Айнет.</p>
  <p id="j6Rv">У каждого должно быть право на выбор, поэтому для всех, кому не хватает базового пакета, мы предлагаем расширенный вариант. В него в дополнение к базовому пакету входят произведения, не являющиеся предметом массового интереса, а также ресурсы Айнет специального назначения. За расширенный информационный пакет мы берём абонентскую плату. Она не слишком большая, чтобы её мог себе позволить каждый, кому требуется информация, но и не слишком маленькая, чтобы удовлетворять чьё-то праздное любопытство.</p>
  <p id="FGlT">Мы в корпорации АйГейпл уважаем право авторов на получение вознаграждения за труд, поэтому исправно платим все авторские отчисления и обеспечиваем доступ к самым новым произведениям для всех желающих в рамках платного информационного пакета «Новинки»…</p>
  <p id="0lwb">Ирвин зевнул. Большую часть из того, о чём рассказывал мужчина в очках, он знал раньше. Только перемотать видеопослание было нельзя, он уже пытался сделать перематывающий жест рукой, но получил сообщение о том, что содержимое защищено от перемотки.</p>
  <p id="FuJY">С пакетом «Новинка» он познакомился в лицее. У всех его одноклассников этот пакет был подключен, в то время как у Ирвина, разумеется, его не было. Это было ещё одним поводом для насмешек, которые мальчик в изобилии получал в свой адрес.</p>
  <hr />
  <p id="QsLH">Учиться в лицее Ирвину очень нравилось, если бы не насмешки одноклассников. После ареста отца мальчик стал главной мишенью для разных жестоких шуток. Поводом для издевательств становилось абсолютно всё — от его одежды и устаревшей прошивки на скинекте до отсутствия подписки на «Новинки». Каждый день начинался примерно одинаково.</p>
  <p id="qQd0">— Эй, Купер, как тебе вчерашняя серия «Хакеров АйНета»? — с деланным интересом обращался к нему Ник Кимбол — упитанный розовощёкий блондин, сын мелкого начальника одного из подразделений АйГейпл.</p>
  <p id="YAzb">— Мне кажется, ему не понравилось, — подхватывал его дружок по фамилии Гилмор. — В квартале имени Збиковски вам не найти камней Сваровски. Как и подписки на «Новинки», правда, Купер? У вас там хоть электричество есть? Или его, как воду в душе по расписанию включают?</p>
  <p id="MaB1">Все присутствующие разражались хохотом. Компания Кимбола продолжала подкалывать Ирвина весь день. Особенно неприятными насмешки становились после того, как Ирвин получал очередную отличную оценку: несмотря на звёздные замашки его мучители далеко не были звёздами в учёбе, зато мастерски изобретали бесконечные способы досадить выскочке из квартала бедняков.</p>
  <p id="Zj8N">Одноклассники не только портили Ирвину настроение, но и создавали реальные сложности. Например, он не мог начать делать домашнее задание, пока ехал из лицея домой на скоростном экспрессе из-за того, что планоут постоянно показывал заставку с непристойной надписью. Его мучители каждый день устанавливали эти надписи с помощью функции, появившейся в новой платной прошивке скинекта, которой у Ирвина, разумеется не было: его скинект устанавливал только исправления ошибок. Картинка пропадала лишь когда мальчик попадал в зону действия сети квартала Марка Збиковски: дистанционная трансляция изображений здесь не поддерживалась.</p>
  <hr />
  <p id="J56T">Однажды, вернувшись из лицея, Ирвин в очередной раз вспомнил о странном жесте, который сделал отец во время ареста и попытался повторить его: собрал пальцы в щепоть, трижды нарисовал ими круг и ткнул в центр. И вдруг… Крошечная комната социального жилья погрузилась в темноту. Среди полной темноты — окон в социальном жилье не предусмотрено — тускло засветился голоэкран планоута. Над экраном появилось … лицо отца. Отец улыбнулся и подмигнул Ирвину.</p>
  <p id="ThUm">— Привет, сынок! Я рад, что ты, наконец, догадался.</p>
  <p id="QHzH">— Папа… — Ирвин был настолько потрясён, что потерял дар речи.</p>
  <p id="iJ0b">— Да, это я. А ты, я смотрю, стал таким взрослым. По моим подсчётам ты учишься уже в восьмом классе. Я не ошибся?</p>
  <p id="fTIK">— В восьмом… — эхом повторил Ирвин.</p>
  <p id="LHDz">— И как твои успехи в учёбе? Алгебра, геометрия, теория вероятности, основы криптографии, информатика, алгоритмы и структуры данных</p>
  <p id="Wae0">— Дда, всё кроме криптографии и алгоритмов. На информатике мы изучаем iBasic.</p>
  <p id="4rYT">— Только iBasic и всё?? — возмутился отец. — Да они что, совсем с ума посходили</p>
  <p id="9Bql">— Ещё нас учат рисовать в айПаинте, — попытался оправдаться Ирвин. — Я разговаривал с соседскими ребятами, которые учатся в обычной школе. У них нет ничего подобного. Их учат только работать в Айнете, делать покупки в магазине АйГейпл и оформлять подписку на «Новинки».</p>
  <p id="gSd0">Мальчика не покидало чувство нереальности происходящего. Отец, конечно, хорош — вместо того, чтобы спросить, как он живёт, выясняет подробности его обучения.</p>
  <p id="dhEh">— Ты не обижайся, сынок, что я не спрашиваю, как ты жил все эти годы. Я наблюдал за тобой по мере возможности. Если можно так сказать о возможностях, которые есть у меня, как у заключённого. Так что я знаю, что ты живёшь здесь все эти годы, что тебя кормят, одевают и даже проявляют какое-то подобие заботы раз в неделю. И ещё я знаю, что ты так и не обзавёлся друзьями. Что, среди одноклассников не нашлось никого, кого ты мог бы назвать своим другом?</p>
  <p id="o2Sd">— Они все ненавидят и презирают меня. Потому что я учусь лучше, чем они, хотя у них есть всё, что они пожелают, а у меня — ничего кроме этой комнаты и планоута… Даже скинект у меня древний, как хобот мамонта…</p>
  <p id="zl7x">— Но-но, поосторожнее говори про скинект, он может и обидеться, — улыбнулся отец. — Я, помнится, встроил в него довольно продвинутый искусственный интеллект, и всё, что тебе нужно — это правильно к нему обратиться.</p>
  <p id="9WIO">— Искусственный интеллект? Разве это уже возможно?</p>
  <p id="9qPA">— Если что-то выглядит, как утка, плавает, как утка и крякает, как утка, то скорее всего, это утка. Проще говоря, неважно, насколько это возможно теоретически, главное, что практически это работает и вполне успешно.</p>
  <p id="ZCOF">— Пап, а почему я всё ещё учусь в лицее имени Джобса? Почему меня не перевели в обычную школу, ведь из нашей квартиры меня выселили?</p>
  <p id="cxxV">— Сынок, я в своё время оказал очень большую услугу корпорации АйГейпл — написал для них базовую систему управления скинектом. Это операционная система в миниатюре, без которой скинект не может работать. TuneOS — операционная система скинекта, с которой имеют дело пользователи, работает уже поверх этой мини-ОС. За это мне полагалось очень хорошее вознаграждение в виде отчислений за каждый подключенный скинект. Я решил, что мне ни к чему столько денег сразу, поэтому договорился, что компания заключит договор с лицеем Стива Джобса, и пока ты там учишься, всё вознаграждение будет перечисляться на счёт лицея. По моим расчётам этих денег было бы достаточно для обучения пятерых учеников, но к чему мелочиться? Поскольку договор заключался не от моего имени, ты до сих пор продолжаешь учиться в лицее. Сумма оказалась настолько внушительной, что лицей проигнорировал мой арест. Когда закончишь учёбу, деньги будут поступать на твой личный счёт. Подожди благодарить! Слушай внимательно, что тебе нужно сделать…</p>
  <p id="vdK6">Отец рассказал Ирвину, что на его скинекте установлена специальная операционная система — LuxOS, которая для непосвященного выглядит как самая обычная TuneOS. Отличие в том, что эта система позволяет делать значительно больше, чем TuneOS. LuxOS не передаёт информацию о действиях пользователя в АйГейпл, в ней можно свободно читать книги, слушать любую музыку, смотреть любые фильмы и писать программы не только на iBasic.</p>
  <p id="ImbL">Это понравилось Ирвину больше всего. Свобода — это то, чего ему очень не хватало. А еще не хватало информации, поэтому он с восторгом принял лозунг «Information must be free» («Информация должна быть свободной»), ведь корпорация АйГейпл ограничила доступ к техническим сведениям о сетевых протоколах, устройстве операционных систем и даже к языкам программирования за исключением iBasic.</p>
  <p id="H5PX">Айбейсик годился лишь на обучение самым основам программирования типа вывода строки «Привет, мир!» или простейшего калькулятора. Что-то серьёзное написать на нём было невозможно. К тому же айбейсик-программы выполнялись лишь на том компьютере, где были написаны. Чтобы распространять программу, требовалось приобрести у АйГейпл лицензию разработчика, потом зарегистрировать программу в магазине приложений — за деньги, разумеется. Плата за регистрацию программы совсем не означала, что программа будет принята в магазин. Да и лицензию разработчика давали крайне неохотно после многочисленных проверок.</p>
  <p id="5oqA">Ещё Ирвину не хватало учебников. В разделе «программирование» официального магазина АйГейпл присутствовали исключительно руководства по iBasic с издевательской пометкой «для профессионалов». Но и на их покупку денег у Ирвина всё равно не было.</p>
  <p id="bBq4">Неудивительно, что LuxOS вместе с многочисленными книгами по программированию и документацией по операционным системам и сетевым протоколам, да ещё и с целой коллекцией компиляторов в комплекте, стала для Ирвина настоящим подарком. Он горячо поблагодарил отца и согласно кивнул, когда отец предупредил его о мерах предосторожности: никогда не работать с визуальным интерфейсом LuxOS за пределами комнаты, никому не рассказывать о том, что он узнал и самое сложное — никак не проявлять свой внезапно выросший уровень знаний.</p>
  <hr />
  <p id="hI19">Ирвин поглощал информацию с немыслимой быстротой. Его мозг словно был создан для этого. Сетевые протоколы и языки программирования — всё отпечатывалось в памяти с первого же прочтения. Уже через месяц он начал писать программы на Python — мощном языке программирования, который очень любили хакеры из-за огромного количества библиотек на все случаи жизни. Ещё он старательно изучал Stift — основной язык для разработки программ в TuneOS. Помимо программирования он вникал в устройство операционных систем и аппаратной платформы техники АйГейпл. Теперь Ирвин понимал, почему айбейсик казался ему таким примитивным.</p>
  <p id="AW4o">Сетевые протоколы — набор правил, по которым все устройства в сети взаимодействуют между собой — поддались ему не сразу. Но упорное изучение документации сыграло свою роль. Скоро IPv4, IPv6, IPvX, — всё, о чём раньше Ирвин не имел ни малейшего представления, стали для него обыденностью. Он научился работать с сетевыми анализаторами и фильтрами пакетов, его программы оказывались намного эффективнее тех, что считались эталонными.</p>
  <hr />
  <p id="MQzp">В качестве пробы сил Ирвин решил поставить на место одноклассников, которые продолжали досаждать ему. На скинекте ждала своего часа небольшая программа, написанная специально для этого случая. В качестве цели мальчик выбрал Кимбола с компанией, поскольку их пакости досаждали ему сильнее всего.</p>
  <p id="mSqK">В один из дней Ирвин пришёл в лицей чуть раньше и уселся на место Кимбола. Это было больше, чем просто вызов: места в классе ученики лицея занимали в соответствии с негласным табелем о рангах, так что Ирвину всегда приходилось ютиться на самой задней парте.</p>
  <p id="VAXP">Кимбол обычно приходил самым последним. Его имиджмейкер считал, что люди достойные никогда не приходят слишком рано. Их удел — появляться точно вовремя, чтобы плебс, пришедший заранее, с завистью наблюдал за их появлением.</p>
  <p id="KvPa">Так было и в этот раз. За десять секунд до начала урока Кимбол появился на пороге класса. Увидев на своём месте Ирвина, он побагровел, но из-за недостатка времени ничего не смог сделать, так что ему пришлось занять место на последней парте.</p>
  <p id="WchW">Ирвин запустил программу-снифер собственной разработки и в течение урока с удовлетворением следил за злобными репликами Кимбола в общем чате класса. Раньше Ирвин не мог получить доступ к этому чату из-за ограничений скинекта. Зато сейчас он мог не только участвовать в общем разговоре от своего имени, но и написать сообщение от имени любого из одноклассников.</p>
  <p id="pOun">Напряжение в чате нарастало, и к концу урока достигло наивысшей точки. Когда мелодичный звук колокольчика пригласил учеников на перерыв, Кимбол вскочил, как ужаленный.</p>
  <p id="N3Nj">— Купер! Кто тебе позволил занять моё место!</p>
  <p id="JN5s">— Кимбол, разве на этом месте висит табличка с твоим именем? Оно такое же твоё, как и моё, — спокойно ответил ему Ирвин.</p>
  <p id="tLJ9">— Ну, Купер, ты сам напросился, — злобно пропел Кимбол и подал знак своим прихвостням — Гилмору, Сеймуру и МакКарти. Обычно после этого над планоутом Ирвина появлялась какая-нибудь непристойная картинка. Выключить её у Ирвина не получалось, потому что его скинект уступал по возможностям скинектам одноклассников. В результате он весь день получал замечания от учителей и пониженный балл за поведение. Только в этот раз всё пошло не так.</p>
  <p id="zbCW">После знака Кимбола ничего не произошло. Совсем. Троица подпевал усиленно мотала руками, пытаясь активизировать атакующую программу, но безуспешно. А потом над планоутами всех четверых засветились объёмные надписи «Я придурок». И все попытки агрессоров отключить эту надпись ни к чему не привели. Как и попытки учителя информатики, который появился к началу урока.</p>
  <p id="77bl">— Это всё Купер виноват, — пытался обвинить его Кимбол.</p>
  <p id="jtKV">Но Ирвин лишь непонимающе пожимал плечами, мол какие дела, у меня скинект последний раз обновлялся восемь лет назад.</p>
  <p id="AUDY">Чтобы избавиться от надписей, одноклассникам пришлось оплачивать замену прошивки скинекта. После этого случая Кимбол стал вести себя более осторожно, не проявляя прямой агрессии, но и не упуская случая как-нибудь съязвить в адрес Ирвина.</p>
  <hr />
  <p id="VNfw">Разделавшись с обидчиками, Ирвин был разочарован. С одной стороны, он получил то, что хотел: к нему больше не цеплялись, и он мог спокойно учиться. С другой — он не только не испытал облегчения, а напротив, ощущал стыд из-за того, что так глупо использовал знания, полученные благодаря отцу. Поддавшись эмоциям, он сильно рисковал. Кто знает, к каким последствиям может привести его поступок в дальнейшем.</p>
  <p id="jxGa">Он почувствовал себя обязанным сделать что-то важное и нужное для всех. Изменить мир, созданный корпорацией АйГейпл, дать всем по-настоящему равные возможности. Это чувство оформилось в цель, и он пришёл к пониманию того, что должен сделать. Он вернёт всем свободу, которую забрала АйГейпл в обмен на сытую и комфортную жизнь.</p>
  <p id="MgFB">Каждый день Ирвина наполнился смыслом. По дороге в лицей и обратно он изучал документацию по внутреннему устройству мини-ОС, вчитывался в комментарии к программному коду, написанные отцом. Это так увлекало, что он с большим сожалением переключал планоут из секретного режима в обычный, когда скинект легкой вибрацией напоминал, что пора готовиться к выходу.</p>
  <p id="InGq">Учёба в лицее стала лишь досадной помехой в реализации планов Ирвина. Он по-прежнему получал хорошие оценки, ведь материал, который давали преподаватели, оказался жутко примитивен по сравнению с теми знаниями, которые он получал из учебников на секретном разделе своего скинекта. И самое главное — многие вещи оказались откровенной неправдой.</p>
  <hr />
  <p id="MOAc">Первое, что следовало сделать, вернувшись из лицея домой, это взять свой ужин в автомате. Автомат-фидер был один, поэтому в завтрак и ужин к нему выстраивалась длиннющая очередь. Ирвин обычно стоял рядом с соседскими мальчишками-близнецами африканского происхождения. Чак и Боб, белозубые и жизнерадостные ребята, увлекались хип-хопом и баскетболом. Они развлекали стоящих в очереди, напевая на разные лады фрагмент очередной популярной песенки. Поскольку для того, чтобы услышать её целиком, нужна была платная подписка, продолжение песенки братья придумывали сами. Эти импровизации очень нравились всем соседям, и братья получали свою заслуженную порцию аплодисментов.</p>
  <p id="dwwh">Ирвин как-то спросил у Чака, чего ему не хватает для счастья. Тот немного подумал и сказал: «Пожалуй, у меня есть всё, что нужно. Мне бы только ещё послушать, что там Айк Эм Си сочинил в своём новом альбоме, тогда я был бы вполне доволен!»</p>
  <hr />
  <p id="t3z3">Получив свою порцию, Ирвин заходил домой, быстро ужинал, делал уроки и до поздней ночи работал над модвормом — модификацией системной прошивки скинекта со встроенным механизмом распространения. В процессе размножения модворм корректировал правила на сетевых фильтрах, сенсорах и маршрутизаторах. Совсем чуть-чуть, так, чтобы не сработала тревожная сигнализация.</p>
  <p id="Vlfe">Отлаживать модворм было очень сложно. Пришлось организовать целую ферму виртуальных машин внутри скинекта. Ресурсов не хватало, и даже «разгон» процессора помогал совсем немного. Браслет обжигал руку так, что было невозможно терпеть, но Ирвин заставлял себя не обращать внимания на боль. Ночью он забывался тревожным сном. Ему снился отец. Он о чём-то пытался его предупредить, но Ирвин никак не мог разобрать слова…</p>
  <hr />
  <p id="0atB">Через несколько месяцев напряжённой работы все баги в программе были отловлены, а набор функций получился даже богаче изначально запланированного. Виртуальные скинекты, маршрутизаторы, шлюзы и системы глубокой инспекции трафика покорно сдавались на милость модворма. Можно было запускать его на просторы Айнета и наблюдать за тем, как меняется мир.</p>
  <p id="9ENq">Мальчик долго думал, в какое время лучше выпустить программу на свободу, и решил, что сделает это по дороге на занятия в день годовщины ареста отца. За час модворм распространится достаточно для того, чтобы это стало заметно. А это значит, что когда Ирвин зайдёт в лицей, большая часть его одноклассников уже будет иметь модифицированную мини-ОС в скинектах. Ещё через час модификация установится у всех жителей мегаполиса, а к концу дня все граждане АйГейпл получат полную свободу.</p>
  <p id="wBIf">Модворм, написанный Ирвином, отключал механизмы слежения за пользователями и ограничения доступа к контенту. Фактически, он превратил жёстко регламентированный Айнет в свободную от ограничений сеть, которую мальчик про себя называл Фринетом.</p>
  <p id="pu1C">Пользователи Фринета получали неограниченный бесплатный доступ к музыке, книгам и фильмам. Они могли делать всё, что угодно на своих скинектах, не опасаясь, что копирайт-полиция АйГейпл обнаружит какие-либо нарушения.</p>
  <p id="PT0O">Ирвин даже записал небольшой ролик, в котором рассказывал о свободе, о том, что информация стала доступна для всех без ограничений. Модворм запускал этот ролик после установки модернизированной прошивки на устройстве отображения, присоединённом к скинекту.</p>
  <hr />
  <p id="bcDT">Двери вагона закрылись, скоростной экспресс бесшумно полетел над монорельсом. Ирвин глубоко вдохнул, зажмурился, а потом сделал особый жест тремя пальцами левой руки. «За отца!» — подумал он, медленно выдыхая. Модворм начал свою работу, взламывая и модифицируя прошивки всех устройств, оказавшихся в зоне его охвата.</p>
  <p id="A37A">Над раскрытым планоутом мужчины в строгом костюме в другом конце вагона появилась картинка видеоролика, запущенного модвормом. Мужчина задёргался, пытаясь остановить воспроизведение, но у него ничего не получилось. Дослушав ролик до конца, мужчина принялся оглядываться по сторонам с затравленным выражением лица, затем судорожно застучал по клавишам планоута. Постепенно его движения стали более плавными. Наконец, он успокоился, закрыл планоут, достал из кармана носовой платок, вытер лоб и стал смотреть в окно…</p>
  <p id="K26U">«Мимо цели», — подумал Ирвин, выходя из вагона.</p>
  <hr />
  <p id="Q7Qt">В лицее царило оживление. Одноклассники обсуждали сообщение от партии Фринета — так Ирвин назвался в видеоролике. Обсуждение сводилось к тому, можно ли верить анониму, который пообещал всем свободу.</p>
  <p id="Kbbk">— А ты пробовал? — С раскрасневшимся лицом орал Кимбол на МакКарти. — Нет? А чего? Тоже боишься, что это проверка лояльности, устроенная копирайт-полицией? Вот то-то и оно! Проще заплатить и спать спокойно, чем загреметь под фанфары, как некоторые… — С опаской покосился он в сторону Ирвина.</p>
  <p id="PZ0B">Учителя были встревожены не на шутку. Они не знали, что ответить на вопросы учеников. Они не могли спросить об этому у руководства напрямую, поскольку вопрос щекотливый. Руководство тоже не спешило давать разъяснения, ведь это означало бы принятие ответственности. К концу уроков стало понятно, что ученики и преподаватели не спешат воспользоваться предоставленной им свободой, предпочитая отсидеться в кустах.</p>
  <hr />
  <p id="4PpK">Ирвин грустным взглядом проводил скоростной экспресс, с тоскливым воем унесшийся прочь. Квартал имени Марка Збиковски встретил его так, словно ничего не произошло. На качелях у серой коробки общежития катались дети, на пятачке асфальта с баскетбольным кольцом азартно стучали мячом Чак и Боб. У подъезда сидели старушки, возраст которых не позволял им переехать в дом престарелых.</p>
  <p id="CzEa">— Привет, Ирвин! — махнул ему рукой Чак. — Ты сегодня рано. Отпустили пораньше?</p>
  <p id="es5t">— Привет, Чак… Да, отпустили, — пробормотал Ирвин. — Скажи, Чак, а ты получал сегодня сообщение?</p>
  <p id="WC6U">— Какое сообщение? Ты про этот ролик от какого-то чудика, который сказал, что теперь всю музыку можно слушать бесплатно и по сколько хочешь раз. Мы с Бобом даже зашли и послушали новый альбом Айка. Чел реально качает, мы прониклись. Только потом, знаешь, как-то не по себе стало. Словно мы чужое берём… Но ведь мы не такие, верно я говорю, Боб?</p>
  <p id="T8qR">Боб что-то пробурчал в подтверждение, мол, да, не такие мы, нам чужого не надо.</p>
  <p id="hxzd">— Вот я и подумал, — продолжал Чак, — что лучше мы честно заработаем денег и купим альбом Айка. И тогда отожгём вместе с ним по полной, так ведь, Бобби?</p>
  <hr />
  <p id="t88U">У Ирвина не осталось сил стоять в очереди за едой. Да и есть совсем не хотелось. Он зашёл в свою конуру, откинул стол, раскрыл планоут, подключился к сети и прошёлся по новостным каналам в поисках сообщения о вирусе, но нашёл только пародию на свой ролик от юмористического портала. Тогда он запустил управляющий модуль модворма и проверил состояние.</p>
  <p id="RNNU"><strong>Уровень охвата: 99,99%</strong> — модворм установился практически на все устройства, подконтрольные АйГейпл.</p>
  <p id="tXcG"><strong>Обращений за свободным доступом: 0,01%</strong> — только один человек из десяти тысяч попробовал обратиться к «закрытому» прежде контенту.</p>
  <p id="GEud"><strong>Повторных обращений за свободным доступом: 0%.</strong></p>
  <p id="upHn">Ирвин в отчаянии уронил голову на руки. Он едва сдерживал слёзы. Свобода оказалась никому не нужна.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/linker</guid><link>https://azhark.cc/linker?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/linker?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Линкер: самый недооценённый этап сборки</title><pubDate>Sat, 18 Apr 2026 18:32:42 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/26/dd/26ddabe6-0372-4f06-86e4-98fe52676192.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/63/ab/63abb89a-b793-4589-ab5e-67bc29ad9540.png"></img>Представьте картину. Разработчик Chromium меняет в исходниках одну строчку. Нажимает «собрать». Встаёт, идёт на кухню, заваривает чай. Возвращается — сборка всё ещё идёт. Линкер.]]></description><content:encoded><![CDATA[
  <figure id="NstV" class="m_original">
    <img src="https://img3.teletype.in/files/63/ab/63abb89a-b793-4589-ab5e-67bc29ad9540.png" width="1672" />
  </figure>
  <p id="wdjr">Представьте картину. Разработчик Chromium меняет в исходниках одну строчку. Нажимает «собрать». Встаёт, идёт на кухню, заваривает чай. Возвращается — сборка всё ещё идёт. Линкер.</p>
  <p id="aMAx">Это не анекдот. В 2020 году на среднем рабочем ноутбуке финальная линковка Chromium через GNU ld занимала около минуты. Через LLVM lld — около одиннадцати секунд. Казалось, что одиннадцать — это предельно быстро: Руи Уэяма, автор lld, потратил на эту цифру годы оптимизаций. Когда вы касаетесь маленькой функции в <code>.cc</code>-файле, компилятор пересобирает один-два объекта за полсекунды, а потом линкер — он не знает, что изменилась строчка — заново перечитывает все тридцать тысяч объектных файлов. Тридцать. Тысяч. И каждый раз проходит по всей программе от начала до конца.</p>
  <p id="1UdO">В июне 2021 года этот же Руи Уэяма выкладывает на GitHub новый проект — <strong>mold</strong>. И линкует Chromium <strong>за две секунды</strong>.</p>
  <p id="K1jU">Точные числа из его бенчмарков на 12-ядерной машине:</p>
  <figure id="gO0z" class="m_retina">
    <img src="https://img4.teletype.in/files/f9/26/f926a009-163c-4386-9110-fd410e171690.png" width="791" />
  </figure>
  <p id="3o7e">Reddit, Hacker News, внутренние чаты Google и Meta просто взрываются. В двадцать пять раз. Не на двадцать пять процентов, а в двадцать пять раз. Без волшебства, без читов, на тех же файлах, на том же железе.</p>
  <p id="NJGX">Больше половины комментариев под первой новостью — вариации одной и той же фразы: «А что, линковка вообще может быть такой быстрой?». Это и есть главный симптом проблемы, о которой мы поговорим. Линкер — невидимый, «последний этап», про который никто не думает. Компилятор мы обсуждаем часами (SSA, register allocation, optimization passes). Линкер — просто пробел между сборкой и запуском.</p>
  <p id="mDXa">А ведь от него зависит всё: автономен ли бинарник, запустится ли он на соседнем сервере, сколько времени уходит на <code>cargo build</code>, возможна ли кросс-компиляция без мучений с sysroot, заработает ли Docker-образ размером в пять мегабайт. Эта статья — про тихого диктатора, без которого невозможен ни один запущенный исполняемый файл на вашей машине. С разбором PLT/GOT, с историей от EDSAC до Wild, с кодом на Rust, Go, Zig и Nim.</p>
  <h1 id="часть-1-почему-линкер-вообще-нужен">Часть 1. Почему линкер вообще нужен</h1>
  <h2 id="игрушечная-модель-vs-реальность">Игрушечная модель vs реальность</h2>
  <p id="fokr">Представьте, что вся ваша программа — один файл на 200 строк. Компилятор читает этот файл, генерирует машинный код, оборачивает его в заголовок исполняемого файла (ELF, Mach-O, PE/COFF — смотря какая ОС), и готово. Линкер как будто не нужен.</p>
  <p id="JTv6">Но в жизни так не бывает. Ядро Linux — <strong>36 миллионов</strong> строк кода. Chromium — <strong>35 миллионов</strong>. LLVM — <strong>10 миллионов</strong>. Ваш Cargo-проект со всеми зависимостями запросто набирает пару миллионов через <code>serde</code>, <code>tokio</code>, <code>reqwest</code>. Скомпилировать эту громаду одним куском означает ждать часами каждый раз, когда вы меняете одну строку.</p>
  <p id="VXaS">Ровно для этого в 1950-х придумали раздельную компиляцию: каждый исходник превращается в полуфабрикат — <em>объектный файл</em>. В нём уже машинный код, но он ещё не знает, где находятся функции из других файлов. Ссылки на них — «дырки», заполненные нулями с пометкой «сюда вставить адрес <code>printf</code>, когда он станет известен». Эти полуфабрикаты собирает в единое целое линкер.</p>
  <p id="6pIf">Объектный файл — это, по сути, <strong>машинный код с дырками</strong>. Линкер эти дырки заклеивает. Если за час работы вы меняете два файла, компилятор пересобирает два объектных файла (секунды), а линкер быстро пересобирает финальный бинарник из свежих и старых объектников.</p>
  <h2 id="что-реально-лежит-внутри-объектного-файла">Что реально лежит внутри объектного файла</h2>
  <p id="HPsI">Возьмём для примера самый простой случай. Файл <code>math.nim</code>:</p>
  <pre id="GCrY">proc square*(x: int): int = x * x
proc cube*(x: int):   int = x * square(x)
</pre>
  <p id="kJ9O">Nim транслирует это в C, GCC/Clang компилирует в <code>math.o</code>. Посмотрим внутрь:</p>
  <ul id="3i3j">
    <li id="fYOr"><strong>Секция <code>.text</code></strong> — машинный код функций <code>square</code> и <code>cube</code>. Это собственно программа.</li>
    <li id="kV0p"><strong>Секция <code>.rodata</code></strong> — read-only data: строковые литералы, константы, таблицы.</li>
    <li id="p2Ew"><strong>Секция <code>.data</code></strong> — инициализированные глобальные переменные.</li>
    <li id="8IRB"><strong>Секция <code>.bss</code></strong> — нулевые глобалки (в файле не хранятся, только «зарезервировать N байт»).</li>
    <li id="fQri"><strong>Таблица символов <code>.symtab</code></strong> — список имён с адресами: «функция <code>square</code> начинается по смещению 0x10, имеет размер 0x24 байта, локальная/глобальная видимость, такого-то типа».</li>
    <li id="iGso"><strong>Таблица релокаций <code>.rela.text</code></strong> — самое интересное. Для каждой «дырки» в коде здесь запись: «по смещению 0x37 в .text подставить адрес символа <code>square</code>, относительный, 32-битный».</li>
  </ul>
  <p id="bZLJ">Вот как посмотреть это на реальном бинарнике:</p>
  <pre id="YBI3">nm ./link_demo | head -5      # таблица символов
#  0000000100004000 d _nim_program_result
#  00000001000015f0 T _NimMainInner
#  00000001000015a0 T _NimMain
#  000000010000cf70 t _my_add       ← наша функция, экспортированная
#  000000010000d370 T _main
readelf -r math.o              # релокации
objdump -d math.o              # дизассемблер
</pre>
  <p id="z6N7">В <code>nm</code> буква <code>T</code> означает «экспортированный символ в секции кода». Маленькая <code>t</code> — локальный (не виден снаружи). <code>D</code> и <code>B</code> — глобальные переменные. <code>U</code> — <em>undefined</em>, то самое «меня нет в этом файле, линкер, найди меня где-нибудь ещё».</p>
  <h2 id="три-задачи-линкера">Три задачи линкера</h2>
  <p id="OQIo">Формально линкер решает ровно три задачи. Всё остальное — разнообразие реализаций.</p>
  <p id="Q3ak"><strong>1. Разрешение символов (<em>symbol resolution</em>).</strong> Линкер берёт все объектные файлы и все указанные библиотеки, строит глобальную таблицу: какой символ где живёт. Для каждого <code>U</code>-символа нужно найти соответствующий <code>T</code> или <code>D</code> в каком-то другом файле. Нет подходящего — знаменитая ошибка <code>undefined reference to &#x27;foo&#x27;</code>. Нашёл два определения одного символа — <code>multiple definition of &#x27;foo&#x27;</code>.</p>
  <p id="9nc8">Есть ещё <strong>слабые символы</strong> (weak) — особый тип, который позволяет «вот моё определение, но если найдёшь сильное — бери его». Слабые символы используются для переопределения <code>operator new</code> в C++ или замены <code>malloc</code> на jemalloc/tcmalloc. Здесь линкер из арбитра превращается в дипломата.</p>
  <p id="axbe"><strong>2. Размещение (<em>relocation</em>).</strong> После того как все символы найдены, нужно дать каждой секции код и данных окончательный виртуальный адрес. <code>.text</code> — в 0x401000, <code>.data</code> — в 0x402000 и так далее. Затем линкер проходит по таблицам релокаций и подставляет настоящие адреса в «дырки».</p>
  <p id="Rkyu">В зависимости от архитектуры релокации бывают абсолютные (вставить полный 64-битный адрес), относительные к PC (смещение от текущей инструкции), GOT-относительные, TLS-относительные и ещё с пару дюжин разновидностей. Только для x86-64 в ELF описано около 40 типов релокаций.</p>
  <p id="5BP3"><strong>3. Формирование выходного файла.</strong> Склеить всё в формат, понятный операционной системе. На Linux — ELF, на macOS — Mach-O, на Windows — PE/COFF, в браузере — WASM. Форматы разные, идея одна: таблица секций плюс программные заголовки, которые говорят загрузчику «вот это положи в память по такому-то адресу с такими-то правами доступа».</p>
  <p id="XzCo">Звучит прозаично. Но за каждой из трёх задач — полвека инженерных войн и дюжина тонких нюансов, которые определяют, будет ли ваш бинарник работать на соседнем сервере.</p>
  <h1 id="часть-2-семь-десятилетий-истории">Часть 2. Семь десятилетий истории</h1>
  <h2 id="1949-дэвид-уилер-изобретает-подпрограмму-и-линковку">1949: Дэвид Уилер изобретает подпрограмму и линковку</h2>
  <p id="HDAJ">История линкеров начинается не с ENIAC и не с IBM. Она начинается в Кембридже. В мае 1949 года там запустили <strong>EDSAC</strong> — электронную машину с хранимой программой. И практически сразу Дэвид Уилер, молодой аспирант (позднее — соавтор шифра блочного шифрования Feistel и один из создателей операционной системы CAP), столкнулся с очевидной проблемой: одни и те же куски кода — вычисление синуса, печать числа, ввод данных с ленты — переписываются в каждой программе заново. Руками. С пересчётом всех адресов переходов.</p>
  <p id="o1YD">И Уилер придумал две вещи, которые определили всю компьютерную науку дальше.</p>
  <p id="jPh1">Первая вещь — подпрограмма. Уилер изобрёл механизм «вызвать блок кода и вернуться обратно» и придумал название для него — <em>subroutine</em>. Именно его инструкция перехода с сохранением обратного адреса получила имя <em>Wheeler jump</em>. Это то, что сегодня мы называем <code>call</code>/<code>ret</code>.</p>
  <p id="a6My">Вторая вещь — линковка. Уилер написал программу, которая называлась <strong>initial orders</strong>: крошечный загрузчик на одной перфоленте, который читал с другой ленты основную программу, находил в ней «адресные метки», подставлял реальные адреса подпрограмм и клал всё в память. Это первый в мировой истории линкер. Он работал на ЭВМ с 512 словами памяти. За это Уилер позже получил премию Тьюринга.</p>
  <h2 id="1952-грейс-хоппер-и-слово-библиотека">1952: Грейс Хоппер и слово «библиотека»</h2>
  <p id="pF9B">В 1952 году Грейс Хоппер — будущая контр-адмирал флота США и крёстная мать COBOL — создаёт <strong>A-0 System</strong>, один из первых трансляторов. Программист пишет на перфокартах номер нужной подпрограммы, A-0 находит её на магнитной ленте, переписывает в программу и правит все адреса. Полноценный линкер, написанный в 1952 году на одной из первых коммерческих ЭВМ.</p>
  <p id="FYqs">Именно отсюда пошло слово <strong>«библиотека»</strong>. Команда Хоппер держала набор готовых подпрограмм на магнитных лентах, физически в шкафу, как книги на полке. Сотрудник подходил, брал нужную ленту, загружал её в машину. Сотрудница назвала эти ленты <em>library</em> — и название пережило 70 лет, перфоленты, саму Хоппер, COBOL и почти всех, кто это видел своими глазами.</p>
  <h2 id="1960-е-ibm-os360-линкер-и-загрузчик-расходятся">1960-е: IBM OS/360, линкер и загрузчик расходятся</h2>
  <p id="1IAb">В IBM OS/360 впервые появляется каноническое разделение, которое живёт до сих пор:</p>
  <ul id="jjCW">
    <li id="GTNK"><strong>Линкер</strong> — собирает из объектных файлов единый исполняемый модуль на диске.</li>
    <li id="SSCl"><strong>Загрузчик</strong> (<em>loader</em>) — берёт готовый модуль и кладёт его в память при запуске.</li>
  </ul>
  <p id="roD7">Книга Джона Левайна <strong>«Linkers and Loaders»</strong> 1999 года, которая до сих пор считается главной энциклопедией в этой предметной области, называется именно так не случайно: это две разные сущности, которые делают очень похожую работу в разные моменты времени.</p>
  <h2 id="1970-е-кен-томпсон-пишет-ld-для-unix">1970-е: Кен Томпсон пишет <code>ld</code> для Unix</h2>
  <p id="r1sl">В первых версиях Unix появляется утилита <code>ld</code> — <em>link editor</em>. Написал её Кен Томпсон. С тех пор на всех Unix-системах линкер по умолчанию называется просто <code>ld</code>, и это прямое наследие тех времён. Формат объектных файлов — <code>a.out</code>, примитивный и приземлённый (тот же формат дал имя стандартному выходному файлу компилятора: <code>a.out</code> — и сегодня, если забыть <code>-o</code>, вы получите файл с этим именем).</p>
  <h2 id="1988-в-att-рождается-elf">1988: в AT&amp;T рождается ELF</h2>
  <p id="R8K5">К концу 1980-х <code>a.out</code> и COFF (Common Object File Format) трещат по швам: нужна нормальная поддержка разделяемых библиотек, debug-информации, многих архитектур. В 1988 году AT&amp;T System Laboratories публикуют <strong>ELF</strong> — <em>Executable and Linkable Format</em>. Элегантный, расширяемый, одинаковый для объектников и исполняемых файлов. В 1995 году Linux окончательно переходит на ELF, вслед за ним FreeBSD, Solaris, и ELF становится негласным стандартом всех Unix-систем, кроме macOS.</p>
  <h2 id="1990-е-рождение-dll-hell">1990-е: рождение DLL Hell</h2>
  <p id="qFLE">Windows 95, эра CD-ROM, «установите программу — 12 дискет». Системные DLL лежат в одной папке. Установка одной игры может заменить <code>msvcrt.dll</code> на свою, несовместимую с другими приложениями, версию. На форумах появляется термин <strong>DLL Hell</strong> и становится мемом. Microsoft отвечает пакетной артиллерией:</p>
  <ul id="43G6">
    <li id="HEaB"><strong>Windows File Protection</strong> (Windows 2000) — мешает заменять системные DLL.</li>
    <li id="01r0"><strong>Side-by-Side Assemblies / WinSxS</strong> (Windows XP) — несколько версий одной и той же DLL могут сосуществовать в системе.</li>
    <li id="aw5R"><strong>Манифесты</strong> — каждое приложение указывает, какую версию библиотеки оно хочет.</li>
    <li id="EEKo"><strong>.NET GAC</strong> — Global Assembly Cache, своё решение в мире .NET.</li>
  </ul>
  <p id="Rhjn">В итоге — гигантская папка <code>C:\Windows\WinSxS</code> размером в десятки гигабайт, куда сложены все когда-либо установленные версии всех системных библиотек. DLL Hell стал управляемым. Но абсолютно неэлегантным.</p>
  <h2 id="2000-е-dependency-hell-в-unix">2000-е: dependency hell в Unix</h2>
  <p id="9Dbh">В Unix-мире происходит симметричная драма. Программа собрана под <code>libssl.so.1.0.0</code>, а в системе уже <code>libssl.so.3</code>. ABI изменилось, бинарник падает на старте с <code>undefined symbol: EVP_md5</code>. Появляются пакетные менеджеры: <code>apt</code>, <code>rpm</code>, <code>pacman</code>, <code>portage</code>, позднее <code>nix</code>. Они решают проблему — пока вы внутри одного дистрибутива одной версии. Как только нужно взять бинарник, собранный на Ubuntu 20.04, и запустить на CentOS 7 — добро пожаловать в знаменитое <code>/lib/x86_64-linux-gnu/libc.so.6: version GLIBC_2.28 not found</code>.</p>
  <h2 id="2013-2014-docker-меняет-всё">2013-2014: Docker меняет всё</h2>
  <p id="4Ywj">Соломон Хайкс выкладывает Docker. Через полгода за ним подтягивается Kubernetes. Философия «упакуйте приложение со всей своей ОС» даёт ответ dependency hell на уровне совершенно иной абстракции — зачем решать проблемы с линковкой, если можно просто запечатать их в контейнер?</p>
  <p id="4e4r">Но контейнер размером 800 МБ ради одного 12-мегабайтного Go-бинарника раздражает. И здесь приходит Go со своей статикой по умолчанию: бинарник без единой внешней зависимости, контейнер <code>FROM scratch</code>, общий размер образа — пять мегабайт. Маятник истории возвращается к статике.</p>
  <h2 id="2021-2025-гонка-линкеров-выходит-на-новый-виток">2021-2025: гонка линкеров выходит на новый виток</h2>
  <p id="cIKM">Руи Уэяма пишет mold. Meta анонсирует Wild на Rust. Apple переходит на свой новый линкер в Xcode 15. wasm-ld становится стандартом для WebAssembly-компонентов. После тридцати лет спокойной эволюции линкеры снова оказываются полем активной инновации.</p>
  <h1 id="часть-3-статика-vs-динамика-вечный-идеологический-спор">Часть 3. Статика vs динамика: вечный идеологический спор</h1>
  <p id="3zBu">Это главный водораздел в мире сборки. Если не понимаете разницу, не поймёте ничего дальше.</p>
  <h2 id="статическая-линковка">Статическая линковка</h2>
  <p id="oFCq">Весь код библиотек физически вшивается в ваш бинарник. Линкер берёт <code>libssl.a</code> (архив объектных файлов), вытаскивает оттуда только нужные функции и подставляет их прямо в ваш <code>.text</code>. На выходе один файл, который можно скопировать на пустой Alpine Linux без единой библиотеки, и он запустится.</p>
  <p id="ifIa"><strong>Преимущества на практике:</strong></p>
  <ul id="lMPf">
    <li id="qjye"><strong>Предсказуемость.</strong> Собралось — работает. Сегодня. Завтра. В 2035 году, если процессор выживет.</li>
    <li id="aKzC"><strong>Простейший деплой.</strong> <code>scp binary server:/usr/local/bin/</code> — всё. Никаких зависимостей, никакой установки.</li>
    <li id="q9yv"><strong>Иммунитет к DLL Hell.</strong> Версия библиотеки зацементирована в бинарнике навсегда.</li>
    <li id="ExtB"><strong>Старые бинарники работают.</strong> Статически собранный бинарник 2008 года запустится на современном ядре Linux.</li>
    <li id="f2Fr"><strong>Проще профилирование.</strong> Все символы в одном файле, все инлайны видны, вся программа анализируется как единое целое.</li>
  </ul>
  <p id="rxi6"><strong>Недостатки:</strong></p>
  <ul id="lDuB">
    <li id="o9j7"><strong>Размер.</strong> Если сто программ в системе линкуют libc статически — в памяти сто копий libc. На современных машинах с терабайтом RAM это не драматично, но 30 лет назад это было катастрофой.</li>
    <li id="RC74"><strong>Обновление безопасности — пересборка.</strong> Нашли дыру в zlib → пересобрать всё, что её использует. Сорок приложений → сорок пересборок. Компилятор пакетного менеджера не помогает.</li>
    <li id="nzTL"><strong>Лицензионные проблемы с LGPL.</strong> Библиотеки под LGPL (включая сам glibc) формально требуют возможности пересобрать приложение с новой версией библиотеки. Статическая линковка это блокирует. Поэтому glibc статически линковать юридически нельзя — musl подставили как выход.</li>
  </ul>
  <h2 id="динамическая-линковка">Динамическая линковка</h2>
  <p id="kcpT">Ссылка на библиотеку остаётся в бинарнике как обещание: «когда меня запустят, найди <code>libssl.so.3</code>, подгрузи в память, подставь реальные адреса». Загрузчик ОС (<code>/lib64/ld-linux-x86-64.so.2</code> на Linux, <code>dyld</code> на macOS, <code>ntdll.dll</code> на Windows) делает это при запуске программы.</p>
  <p id="07xU"><strong>Преимущества:</strong></p>
  <ul id="oST9">
    <li id="ByAe"><strong>Экономия памяти.</strong> Одна копия библиотеки на всю систему. Физические страницы libc разделяются между всеми процессами через <code>mmap</code>.</li>
    <li id="kGKo"><strong>Обновления через пакетный менеджер.</strong> <code>apt upgrade libssl</code> — и все программы в системе сразу используют пропатченную библиотеку.</li>
    <li id="W1w9"><strong>Плагины.</strong> <code>dlopen()</code> позволяет загружать код в рантайме. Это VST-плагины, расширения редакторов, драйверы устройств.</li>
    <li id="VGNC"><strong>Меньший размер бинарников.</strong> Ваша программа — 200 КБ, libc — где-то в системе.</li>
  </ul>
  <p id="5oXB"><strong>Недостатки:</strong></p>
  <ul id="bGxW">
    <li id="fK5d"><strong>Хрупкость.</strong> Несовместимое обновление библиотеки ломает всё, что от неё зависит.</li>
    <li id="Dm1W"><strong>Медленный старт.</strong> Динамический загрузчик может резолвить сотни символов. На тяжёлых программах это миллисекунды или десятки миллисекунд.</li>
    <li id="MIgI"><strong>Сложный деплой.</strong> «Работает на моей машине» — это в 9 случаях из 10 про динамическую линковку.</li>
    <li id="C53q"><strong>Surface для атак.</strong> <code>LD_PRELOAD</code>, GOT overwrite, DLL hijacking — целая коллекция техник злоупотребления динамической линковкой.</li>
  </ul>
  <h2 id="маятник-истории">Маятник истории</h2>
  <p id="AcaK">В 1990-е динамическая линковка казалась абсолютным будущим. Экономия RAM, централизованные обновления безопасности, плагинные архитектуры — казалось, статическая линковка уйдёт в музей вместе с перфокартами.</p>
  <p id="9K9N">В 2014 случился <strong>Heartbleed</strong>. Критическая дыра в OpenSSL. Динамически слинкованные системы спаслись одним <code>apt upgrade openssl</code>. Статически слинкованные нужно было пересобрать полностью. Каждую программу. Это казалось победным аргументом за динамику.</p>
  <p id="08tT">Но дальше пришёл Docker, пришёл Go, и в 2020-х маятник резко качнулся обратно. Контейнер решает проблему обновлений: пересобираем образ, катим в прод, старый удаляется. Размер 5 МБ против 800 МБ статической Ubuntu внутри контейнера — очевидное преимущество статики. Go сделал статику по умолчанию. Rust, собранный с target <code>x86_64-unknown-linux-musl</code>, — тоже. Zig — тем более. Nim — при желании.</p>
  <p id="by0H">Сегодняшний консенсус где-то посередине:</p>
  <ul id="ol4C">
    <li id="x6HW"><strong>Системные утилиты</strong> — динамическая линковка, легко обновляются пакетным менеджером.</li>
    <li id="EPzi"><strong>Приложения на разделяемых хостингах</strong> — по традиции динамическая.</li>
    <li id="xdz2"><strong>Приложения в контейнерах</strong> — всё чаще полностью статические.</li>
    <li id="1i7l"><strong>CLI-утилиты, которые скачивают как бинарник</strong> (<code>rg</code>, <code>fd</code>, <code>hyperfine</code>, <code>bat</code>) — статика, один файл.</li>
    <li id="ogRl"><strong>Браузеры, игры</strong> — динамическая (огромные размеры, большие объёмы кода).</li>
  </ul>
  <p id="JCbs">Принцип выбора простой: если вы распространяете программу отдельно — статика. Если она устанавливается в составе ОС — динамика.</p>
  <h1 id="часть-4-анатомия-elf-что-на-самом-деле-в-бинарнике">Часть 4. Анатомия ELF: что на самом деле в бинарнике</h1>
  <p id="f0KL">Пора залезть внутрь. Все Linux-примеры будут про ELF, но концепции одни и те же для Mach-O и PE/COFF — меняются только имена.</p>
  <h2 id="сегменты-и-секции">Сегменты и секции</h2>
  <p id="2uP0">В ELF есть две параллельных «карты» файла:</p>
  <ul id="viUF">
    <li id="SoN0"><strong>Секции</strong> (sections) — то, что видит линкер. <code>.text</code>, <code>.data</code>, <code>.rodata</code>, <code>.bss</code>, <code>.symtab</code>, <code>.strtab</code> и ещё с пару десятков. Удобно для сборки.</li>
    <li id="sruK"><strong>Сегменты</strong> (segments) — то, что видит загрузчик ОС. Один сегмент <code>LOAD</code> для кода (read+execute), один <code>LOAD</code> для данных (read+write), <code>INTERP</code> с путём к динамическому загрузчику, <code>DYNAMIC</code> со служебной информацией о рантайм-линковке. Удобно для отображения в память.</li>
  </ul>
  <p id="lqaS">Один сегмент <code>LOAD</code> обычно покрывает несколько секций — например, <code>.text</code>, <code>.rodata</code> и <code>.plt</code> все попадают в один read+execute сегмент.</p>
  <h2 id="ключевые-секции">Ключевые секции</h2>
  <figure id="yd31" class="m_retina">
    <img src="https://img4.teletype.in/files/76/b7/76b76b9c-ec8a-4aa5-a1e9-e6ab2e7739f0.png" width="717" />
  </figure>
  <p id="goQp">Каждая из этих секций — отдельный мир со своими правилами, форматом и назначением. <code>.eh_frame</code>, например, — сложнейший формат на основе DWARF, который описывает, как на каждой инструкции программы восстановить регистры и найти обработчики исключений. Когда в Go или Rust падает паника и вы видите красивый stack trace — это работает <code>.eh_frame</code> плюс отладочная информация.</p>
  <h2 id="dt_needed-rpath-runpath-как-программа-находит-свои-библиотеки">DT_NEEDED, RPATH, RUNPATH: как программа находит свои библиотеки</h2>
  <p id="VBDS">Секция <code>.dynamic</code> — табличка, которую читает загрузчик при запуске. Записи <code>DT_NEEDED</code> перечисляют, какие библиотеки нужны:</p>
  <pre id="tADy">readelf -d ./my_program | grep NEEDED
# 0x0000000000000001 (NEEDED)  Shared library: [libssl.so.3]
# 0x0000000000000001 (NEEDED)  Shared library: [libcrypto.so.3]
# 0x0000000000000001 (NEEDED)  Shared library: [libc.so.6]
</pre>
  <p id="kzO5">Когда ОС запускает программу, первым в память загружается не сам бинарник, а указанный в <code>.interp</code> <strong>динамический загрузчик</strong> (<code>ld.so</code>). Он и делает всю работу:</p>
  <ol id="d5SN">
    <li id="Ukfw">Читает <code>DT_NEEDED</code> — список нужных библиотек.</li>
    <li id="JEo1">Ищет каждую по алгоритму:</li>
    <ul id="KKeZ">
      <li id="gWmF"><code>LD_PRELOAD</code> (переменная окружения — принудительные библиотеки).</li>
      <li id="Cj4e"><code>DT_RPATH</code> (если есть — устаревшее).</li>
      <li id="mAHd"><code>LD_LIBRARY_PATH</code> (переменная окружения).</li>
      <li id="0r6G"><code>DT_RUNPATH</code> (новое, заменило RPATH).</li>
      <li id="vP01"><code>/etc/ld.so.cache</code> (построенная <code>ldconfig</code> таблица всех известных библиотек).</li>
      <li id="T97h"><code>/lib</code> и <code>/usr/lib</code> (стандартные пути).</li>
    </ul>
    <li id="rezU">Отображает каждую найденную <code>.so</code> в память через <code>mmap</code>.</li>
    <li id="LUyD">Разрешает все недостающие символы.</li>
    <li id="u9os">Вызывает конструкторы (<code>DT_INIT</code>, <code>DT_INIT_ARRAY</code>).</li>
    <li id="f2TX">Передаёт управление <code>_start</code> вашего бинарника.</li>
  </ol>
  <p id="1bjm">Порядок поиска критичен для безопасности. Если программа ставится с <code>RUNPATH=./</code>, злоумышленник подсовывает в текущую директорию свою <code>libssl.so.3</code> — и ваша программа загружает чужой код с правами пользователя. Это называется <strong>DLL hijacking</strong> и давно перестало быть теоретическим.</p>
  <h2 id="ld_preload-светлая-и-тёмная-стороны">LD_PRELOAD: светлая и тёмная стороны</h2>
  <p id="8Su6"><code>LD_PRELOAD=/path/to/my.so ./program</code> — заставляет загрузчик подгрузить указанную библиотеку <strong>до всех остальных</strong>. Символы из неё имеют приоритет. Это чудесная вещь:</p>
  <p id="HBUx"><strong>Светлая сторона:</strong></p>
  <ul id="m9EP">
    <li id="0j2q"><strong>Профилирование.</strong> <code>LD_PRELOAD=libprofiler.so</code> — оборачиваем все malloc/free для сбора статистики памяти.</li>
    <li id="gli4"><strong>Трассировка сети.</strong> <code>tsocks</code>, <code>proxychains</code> подменяют <code>connect()</code>, чтобы направить трафик через прокси.</li>
    <li id="7UDM"><strong>jemalloc / tcmalloc.</strong> Подменить системный malloc на более быстрый — одна строчка переменной окружения, без пересборки.</li>
    <li id="YWpp"><strong>Отладка.</strong> <code>LD_PRELOAD</code> обёртки для поиска утечек, гонок данных (ThreadSanitizer, Valgrind частично).</li>
  </ul>
  <p id="tDuc"><strong>Тёмная сторона:</strong></p>
  <ul id="0sGd">
    <li id="FbQU"><strong>Rootkit’ы.</strong> Подменяете <code>readdir()</code> — и ваша директория становится невидимой для <code>ls</code>. Подменяете <code>write()</code> — и логи не пишутся. Классика malware в Linux.</li>
    <li id="cfYD"><strong>Обход проверок.</strong> Подменяете <code>getuid()</code> на «всегда 0» — и многие программы думают, что их запустили от root.</li>
  </ul>
  <p id="IsGD">Именно поэтому <code>LD_PRELOAD</code> в SUID-программах отключена. И именно поэтому в модных нынче безопасных системах (Android, iOS) — никакого <code>LD_PRELOAD</code> нет в принципе.</p>
  <h2 id="symbol-versioning-как-glibc-убивает-ваши-бинарники">Symbol versioning: как glibc убивает ваши бинарники</h2>
  <p id="zse7">Вот вам почти детективная история.</p>
  <p id="lhgK">Вы собираете бинарник на Ubuntu 22.04 (glibc 2.35) и пытаетесь запустить на Debian 10 (glibc 2.28). Ошибка:</p>
  <pre id="eBSp">./mybin: /lib/x86_64-linux-gnu/libc.so.6: version &#x60;GLIBC_2.32&#x27; not found
./mybin: /lib/x86_64-linux-gnu/libc.so.6: version &#x60;GLIBC_2.34&#x27; not found
</pre>
  <p id="uMOC">Почему так? Ведь <code>memcpy</code> был в glibc со времён динозавров, за что ошибка?</p>
  <p id="eYWf">Разгадка в том, что glibc использует <strong>symbol versioning</strong>. Когда поведение функции меняется (например, добавили флаг или поправили недокументированное поведение), GNU не ломает ABI, они создают <strong>новую версию</strong> того же символа. Теперь в libc есть одновременно:</p>
  <ul id="znAY">
    <li id="WwgQ"><code>memcpy@GLIBC_2.2.5</code> — старая.</li>
    <li id="pD3r"><code>memcpy@GLIBC_2.14</code> — новая, с другой семантикой перекрытий буферов.</li>
  </ul>
  <p id="EDWu">Когда вы собираете на Ubuntu 22.04, линкер выбирает <code>memcpy@GLIBC_2.14</code>. Когда вы запускаете на Debian 10 с glibc 2.28, там эта версия есть. А вот если где-то в вашем коде glibc вставила вызов символа, появившегося в 2.32 или 2.34, — финиш. Даже если вы никогда не звали эту функцию явно; она могла вылезти из какой-нибудь <code>fprintf</code> или <code>pthread_create</code>.</p>
  <p id="AJqV">Обходные пути:</p>
  <ul id="CPO5">
    <li id="Iilb"><strong>musl libc</strong> — без symbol versioning, без этой драмы.</li>
    <li id="zCNf"><strong>Сборка на старой системе</strong> — Docker-образы с древним glibc.</li>
    <li id="tPGo"><strong>cargo-zigbuild</strong> — использовать <code>zig cc</code> как линкер, он умеет прицеливаться в glibc определённой версии.</li>
    <li id="RV8v"><strong>Статическая линковка всей libc</strong> (практически возможно только с musl).</li>
  </ul>
  <h1 id="часть-5-plt-и-got-магия-ленивой-линковки">Часть 5. PLT и GOT: магия ленивой линковки</h1>
  <p id="F8yL">А теперь посмотрим, как работает динамический вызов функции на машинном уровне. Это одно из самых элегантных инженерных решений в Unix.</p>
  <h2 id="проблема">Проблема</h2>
  <p id="po4x">Ваш бинарник вызывает <code>printf</code>. В момент компиляции и линковки вы не знаете, где в памяти будет <code>printf</code>, потому что:</p>
  <ol id="P9Ol">
    <li id="NicZ">Libc загрузится по произвольному адресу (ASLR — рандомизация).</li>
    <li id="8EDn">Бинарник может быть PIE (Position Independent Executable) — он сам не знает, куда его загрузили.</li>
  </ol>
  <p id="G4pq">Как же на инструкции <code>call printf</code> подставить адрес?</p>
  <h2 id="решение-два-уровня-непрямой-адресации">Решение: два уровня непрямой адресации</h2>
  <p id="U8Ft">Компилятор генерирует вызов не напрямую, а через два промежуточных слоя:</p>
  <ul id="MjMX">
    <li id="ICdD"><strong>GOT</strong> (<em>Global Offset Table</em>) — таблица указателей на реальные адреса внешних символов. Заполняется динамическим загрузчиком.</li>
    <li id="ysrA"><strong>PLT</strong> (<em>Procedure Linkage Table</em>) — массив маленьких «батутов». Каждая запись — 4-5 инструкций, которые делают косвенный переход через соответствующую ячейку GOT.</li>
  </ul>
  <p id="MGgB">Когда ваш код делает <code>call printf</code>, на самом деле он вызывает <code>printf@plt</code>. Посмотрим, что внутри:</p>
  <pre id="yXQG">printf@plt:
    jmp    QWORD PTR [rip + printf@got]   ; переход по адресу из GOT
    push   0x0                            ; индекс символа
    jmp    _dl_runtime_resolve            ; вызвать резолвер
</pre>
  <p id="29Z3">А в GOT для <code>printf</code> изначально лежит… обратный адрес в PLT (на следующую инструкцию после <code>jmp</code>). То есть при <strong>первом</strong> вызове:</p>
  <ol id="Acji">
    <li id="cNaz">Код: <code>call printf@plt</code>.</li>
    <li id="sr1A">PLT: <code>jmp [printf@got]</code> — но там пока обратный адрес.</li>
    <li id="QA0x">PLT продолжает: <code>push</code> индекса и <code>jmp _dl_runtime_resolve</code>.</li>
    <li id="s1jK">Резолвер находит настоящий адрес <code>printf</code> в libc, <strong>записывает его в GOT</strong> и передаёт туда управление.</li>
    <li id="y72h"><code>printf</code> выполняется и возвращается.</li>
  </ol>
  <p id="raRT">При <strong>втором и всех последующих</strong> вызовах:</p>
  <ol id="ryg5">
    <li id="PSR2">Код: <code>call printf@plt</code>.</li>
    <li id="kkdC">PLT: <code>jmp [printf@got]</code> — там уже настоящий адрес.</li>
    <li id="7VL6">Сразу в <code>printf</code>.</li>
  </ol>
  <p id="jEZH">Вся машинерия резолвинга проскакивается одним переходом. Это называется <strong>lazy binding</strong> — ленивое связывание. Великое изобретение конца 80-х, позволяющее быстро стартовать программы, которые линкуют тысячи символов, но реально используют десятки.</p>
  <h2 id="минус-ленивости-атака-got-overwrite">Минус ленивости: атака GOT overwrite</h2>
  <p id="YRQj">Но тут есть проблема. GOT по умолчанию <strong>записываемая</strong> память, ведь загрузчик должен в неё писать. А если писать в GOT может загрузчик, то и ваш эксплойт тоже может.</p>
  <p id="kZyR">Классическая атака: злоумышленник через buffer overflow получает write primitive, переписывает запись для <code>system@got</code> или <code>exit@got</code>, и при ближайшем вызове ваша программа любезно передаёт управление в shell-код. Вариация, известная как <strong>ret2plt</strong>, помогла обойти ранние версии ASLR.</p>
  <h2 id="relro-и-bind_now-защита-от-got-overwrite">RELRO и BIND_NOW: защита от GOT overwrite</h2>
  <p id="HQ1R">Ответ индустрии — две опции линкера:</p>
  <ul id="NnJI">
    <li id="bP9v"><strong>RELRO</strong> (<em>Read-Only Relocations</em>) — после того, как загрузчик заполнил GOT, пометить её как read-only через <code>mprotect</code>. Злоумышленник больше не может писать.</li>
    <li id="xBhd"><strong>BIND_NOW</strong> — не лениться, резолвить все символы сразу на старте. Медленнее на ~50 мс у большой программы, но GOT полностью готова к моменту запуска <code>main</code>.</li>
  </ul>
  <p id="W9kU">Полная защита называется <strong>Full RELRO</strong> и включается одновременно обе опции:</p>
  <pre id="QSil">gcc -Wl,-z,relro -Wl,-z,now  program.c
</pre>
  <p id="QAg3">В современных Linux (Ubuntu, Fedora, Debian) это дефолт для всех системных бинарников. В Go, Rust, Zig при релиз-сборках — тоже по умолчанию. Проверить можно <code>checksec</code>:</p>
  <pre id="hYmg">RELRO           STACK CANARY   NX      PIE      RPATH    Symbols
Full RELRO      Canary found   NX      PIE      No RPATH No Symbols
</pre>
  <h2 id="нюанс-про-производительность">Нюанс про производительность</h2>
  <p id="5JWv">Есть интересный компромисс: Full RELRO замедляет старт программы на 10-100 мс (надо резолвить тысячи символов сразу). Для долгоживущих процессов (веб-сервер, демон) это копейки. Для консольных утилит, которые запускаются миллион раз в день в скриптах (<code>grep</code>, <code>cat</code>), — ощутимо. Поэтому утилиты из GNU coreutils собираются с <strong>Partial RELRO</strong> — GOT остаётся записываемой, но другие защитные меры включены. Компромисс между безопасностью и скоростью старта.</p>
  <h1 id="часть-6-удаление-мёртвого-кода-и-идентичных-функций">Часть 6. Удаление мёртвого кода и идентичных функций</h1>
  <p id="w7Uw">Не всё, что компилятор скормил линкеру, попадает в финальный бинарник. Современные линкеры активно удаляют балласт, причём иногда агрессивнее, чем вы ожидаете.</p>
  <h2 id="секция-per-function--ffunction-sections">Секция per function: <code>-ffunction-sections</code></h2>
  <p id="vUFT">По умолчанию компилятор складывает весь код одного <code>.c</code>-файла в одну секцию <code>.text</code>. Линкер не может удалить отдельную функцию, не поломав позиции других — они все в одной секции.</p>
  <p id="p3oW">Флаги <code>-ffunction-sections</code> и <code>-fdata-sections</code> заставляют компилятор класть <strong>каждую функцию в свою секцию</strong> (<code>.text.my_func</code>, <code>.text.another_func</code>), а каждую глобалку — в свою. Теперь линкер может оперировать функциями по отдельности.</p>
  <h2 id="garbage-collection-секций---gc-sections">Garbage collection секций: <code>--gc-sections</code></h2>
  <p id="fEql">С <code>-Wl,--gc-sections</code> линкер находит все достижимые секции, стартуя с <code>main</code> (или нескольких явных корней — например, точек входа DSO), и выбрасывает всё, до чего не добрался. Точно как сборщик мусора в рантайме, только на уровне линковки.</p>
  <p id="aUJU">На практике это даёт 10-30% сокращения бинарников из C/C++. В Rust включено по умолчанию. В Go собственный линкер делает DCE в ещё более тонкой гранулярности — на уровне отдельных символов внутри секций. Nim через бэкенд C получает это бесплатно.</p>
  <h2 id="identical-code-folding---icf">Identical Code Folding: <code>--icf</code></h2>
  <p id="oziA">LLD и mold поддерживают ещё одну оптимизацию: <strong>ICF</strong> (<em>Identical Code Folding</em>). Линкер ищет функции, которые скомпилировались в абсолютно одинаковый машинный код, и оставляет только одну копию, а остальные делает её алиасами.</p>
  <p id="KfiE">Кажется, что это редкая ситуация. Но на деле C++ и Rust генерируют тонны идентичных функций через шаблоны и генерики. <code>Vec&lt;u32&gt;::push</code> и <code>Vec&lt;i32&gt;::push</code> на уровне машинного кода могут быть неразличимы. ICF их схлопывает в одну. На Chromium это экономит около 10% размера <code>.text</code>.</p>
  <p id="iLcu">Включается: <code>-Wl,--icf=all</code> (lld, mold). В Rust добавляется через:</p>
  <pre id="PyQE">[profile.release]
strip = true
lto = &quot;thin&quot;
# ICF включается автоматически при strip + lto на lld/mold
</pre>
  <h2 id="гендерные-и-вуду-эффекты">Гендерные и вуду-эффекты</h2>
  <p id="RNOW">Одна из дивных шуток ICF: если две функции идентичны по коду, но идеологически разные (<code>fire_missile</code> и <code>print_warning</code>), их адреса после ICF совпадают. Код типа <code>if (fn_ptr == &amp;fire_missile)</code> может начать работать не так, как вы ожидали. Флаг <code>--icf=safe</code> пытается учитывать сравнения адресов функций, но это остаётся тонкой гранью.</p>
  <h1 id="часть-7-гонка-линкеров-кто-когда-и-зачем">Часть 7. Гонка линкеров: кто, когда и зачем</h1>
  <h2 id="gnu-ld-он-же-ldbfd">GNU ld (он же ld.bfd)</h2>
  <p id="x9Uy">Самый распространённый линкер в мире Unix. Написан на C, использует библиотеку абстракций <strong>BFD</strong> (<em>Binary File Descriptor</em>), которая поддерживает около 70 разных форматов и 50 архитектур — от VAX до z/Architecture.</p>
  <p id="ykgc">BFD — и благословение, и проклятие одновременно. Благословение — GNU ld собирает всё и под всё. Проклятие — BFD настолько многослойная, что добавляет огромный оверхед. Плюс линкер однопоточный. С Chromium разбирается почти минуту. Но для 99% мелких проектов этого хватает и никто не жалуется.</p>
  <h2 id="gnu-gold-2008">GNU gold (2008)</h2>
  <p id="Dtdp">Иэн Тейлор из Google пишет новый линкер ELF на C++. Выбрасывает BFD, фокусируется только на ELF и нескольких архитектурах. В 5 раз быстрее GNU ld. Был включён в binutils в 2008 году.</p>
  <p id="ZYGD">Gold был прорывом своего времени, но к 2020 году развитие фактически остановилось. Тейлор ушёл из Google, в binutils его никто не заменил, параллельно LLVM делал lld, и интерес сообщества сместился туда. В 2024 году Fedora начала обсуждать удаление gold как не поддерживаемого.</p>
  <h2 id="llvm-lld-2015">LLVM lld (2015+)</h2>
  <p id="wm0n">Руи Уэяма, бывший инженер Google, начинает в LLVM новый проект — универсальный линкер. Ключевые цели:</p>
  <ul id="MCcI">
    <li id="IqNh"><strong>Кросс-линкер по умолчанию.</strong> Одна сборка <code>lld</code> линкует и ELF, и PE/COFF, и Mach-O, и WASM. На практике это четыре разных «фронтенда» под общим именем: <code>ld.lld</code>, <code>lld-link</code>, <code>ld64.lld</code>, <code>wasm-ld</code> — одна кодовая база, четыре бинарника.</li>
    <li id="r5AI"><strong>Минимализм кода.</strong> LLD — 21 тысяча строк C++ против 198 тысяч у gold. Проще читать, проще менять, проще находить баги.</li>
    <li id="JEPG"><strong>Быстрее всего что было.</strong> В 2+ раза быстрее gold, параллельный, кэш-дружелюбный.</li>
    <li id="02Ui"><strong>LTO из коробки.</strong> Нет отдельной логики для thinLTO — это первоклассная фича.</li>
  </ul>
  <p id="gjpD">LLD стал дефолтом во FreeBSD (2018), дефолтом для Chrome OS, дефолтом в Android NDK. Rust на Windows использует <code>lld-link</code> вместо MSVC-линкера. На Apple Silicon всё больше проектов уходят на <code>ld64.lld</code> вместо системного. LLD — новая норма везде, кроме консервативного GNU-мира.</p>
  <h2 id="mold-2021-а-можно-ещё-быстрее">mold (2021): а можно ещё быстрее?</h2>
  <p id="b4Qn">Через несколько лет работы над lld Уэяма задаёт себе вопрос: а что, если бы я писал линкер сейчас, с нуля, зная всё, что знаю? Так родился mold.</p>
  <p id="V6pb">Ключевые идеи, которые сделали его настолько быстрым:</p>
  <ol id="Jtry">
    <li id="U7tu"><strong>Максимальный параллелизм на каждой фазе.</strong> Lld имеет последовательные фазы между параллельными блоками. Mold пытается делать параллельно всё что можно.</li>
    <li id="vZs1"><strong>Lock-free структуры данных.</strong> Хэш-таблицы символов используют атомарные операции вместо мьютексов.</li>
    <li id="WRBi"><strong>Mmap вместо read.</strong> Объектные файлы отображаются в память, без копирования.</li>
    <li id="n8a1"><strong>Mmap выходного файла.</strong> Mold создаёт пустой файл нужного размера через <code>ftruncate</code>, мэпит его и пишет напрямую через массивы в памяти — ядро само отправит грязные страницы на диск в фоне, часто уже после завершения линкера.</li>
    <li id="SNkf"><strong>Оптимизированные парсеры ELF.</strong> Написанные с учётом cache line размеров.</li>
    <li id="Lr2E"><strong>Инкрементальная фаза <code>--thinlto-*</code></strong> — максимально параллельная обработка IR в ThinLTO.</li>
  </ol>
  <p id="OxmI">Результат на Chromium (2.1 ГБ входных данных, 30 тысяч объектных файлов, 500 тысяч символов):</p>
  <ul id="PlWW">
    <li id="L32c">GNU ld: 53,86 с.</li>
    <li id="YVC0">gold: 12,96 с.</li>
    <li id="6Oap">lld: 11,03 с.</li>
    <li id="WQPu"><strong>mold: 2,09 с.</strong></li>
  </ul>
  <p id="jitB">Один инженер. Один репозиторий. 26-кратное ускорение против GNU ld. Это тот случай, когда «хорошие инженерные решения» звучит слишком скромно — это переосмысление задачи с чистого листа.</p>
  <p id="d4nT">Интересный поворот лицензии: изначально mold вышел под AGPL (коммерческая лицензия, запрещающая SaaS-замыкание). Одновременно Уэяма делал платный порт для macOS под названием <strong>sold</strong> — за 100 долларов лицензии на машину. В 2024 году он перевёл и mold, и sold под MIT и полностью открыл код. Mold стал стандартом де-факто для ускорения Rust-сборок.</p>
  <h2 id="wild-2024-2025-линкер-на-rust">Wild (2024-2025): линкер на Rust</h2>
  <p id="rvIQ">Meta анонсирует <strong>Wild</strong> — экспериментальный линкер на Rust от Дэвида Латтимора. Цели:</p>
  <ul id="YPzd">
    <li id="psGD">Первоклассная <strong>инкрементальная линковка</strong>: перелинковывать только изменившиеся части бинарника. Это то, о чём давно мечтали все, но никто не сделал нормально — mold, lld делают inкрементальность крайне ограниченно.</li>
    <li id="XRFf"><strong>Hot reload</strong> в перспективе: менять работающую программу на лету.</li>
    <li id="faMb">Проверить, даст ли Rust выигрыш в безопасности и параллелизме для инструмента такого масштаба.</li>
  </ul>
  <p id="lUwU">Wild на ранних бенчмарках близок к mold по скорости и на некоторых сценариях обгоняет благодаря инкрементальности. Проект молодой (2024), но активно развивается.</p>
  <h2 id="apple-ld-prime-xcode-15-2023">Apple ld-prime (Xcode 15, 2023)</h2>
  <p id="1E9u">Пока мир смотрел на mold в Linux, Apple тихо переписывала свой линкер для macOS. В Xcode 15 появился новый <code>ld</code>, примерно в 5 раз быстрее старого на типовых проектах Swift/Objective-C. Детали не публиковались, но из дизассемблирования инженеры реверсят: параллелизм, mmap, кэширование — те же идеи, что в mold, без прямого упоминания mold.</p>
  <h2 id="сводная-таблица">Сводная таблица</h2>
  <figure id="H3Ji" class="m_retina">
    <img src="https://img1.teletype.in/files/82/53/82536aca-1629-401d-80bb-6e7a9f48fb49.png" width="801" />
  </figure>
  <p id="NLoj">Цифры — приблизительные и сильно зависят от проекта. Соотношение между линкерами сохраняется на большинстве больших C++/Rust-проектов.</p>
  <h1 id="часть-8-оптимизации-после-компиляции-lto-pgo-bolt">Часть 8. Оптимизации после компиляции: LTO, PGO, BOLT</h1>
  <p id="L7xO">В классическом пайплайне каждый <code>.c</code>-файл компилируется отдельно, и компилятор видит только его содержимое. Оптимизатор не может заинлайнить функцию из соседнего файла, не может выбросить неиспользуемую глобальную переменную, не может девиртуализовать вызов через границу модуля. Современные тулчейны взламывают эту изоляцию тремя способами.</p>
  <h2 id="lto-видеть-всю-программу-одним-куском">LTO: видеть всю программу одним куском</h2>
  <p id="Ig5x"><strong>Link-Time Optimization</strong> — компилятор вместо машинного кода выдаёт <strong>промежуточное представление</strong> (LLVM IR у Clang/Rust, GIMPLE у GCC, свой формат у Nim). Линкер собирает всё это IR, вызывает оптимизатор на всей программе как на единой трансляционной единице, и только потом из IR получается машинный код.</p>
  <p id="Fvsh"><strong>Fat LTO.</strong> Весь IR грузится в одну гигантскую единицу компиляции. Максимально агрессивные инлайны, выбрасывание мёртвого кода на всю программу, девиртуализация виртуальных вызовов. Платите вы памятью: на проекте в сотни мегабайт IR оптимизатор может съесть 30 ГБ RAM и думать полчаса. Это норма.</p>
  <p id="1uM2"><strong>Thin LTO</strong> (LLVM, 2016). Каждый модуль оптимизируется отдельно, но с доступом к <strong>сводке</strong> (summary) других модулей — краткой информации: имена функций, их размеры, ссылки на внешние символы. Параллельно. Медленнее Fat LTO в 3-5 раз (на финальной скорости кода), но требует в 10 раз меньше памяти и работает в разы быстрее. Thin LTO — разумный компромисс для подавляющего большинства продовых сборок.</p>
  <p id="KOYn">В Rust LTO включается парой строк в <code>Cargo.toml</code>:</p>
  <pre id="nVZJ">[profile.release]
lto = &quot;thin&quot;           # или &quot;fat&quot;, или true
codegen-units = 1      # обычно с LTO ставят 1 единицу кодогенерации
strip = true           # попутно убираем debug-инфу
</pre>
  <p id="nwFd">Разница на реальных проектах: прирост скорости кода 5-20%, но время сборки вырастает в 2-4 раза. Для ежедневной разработки — отключено. Для CI с релизными артефактами — включено.</p>
  <h2 id="pgo-компилятор-учится-на-профиле">PGO: компилятор учится на профиле</h2>
  <p id="PZsr"><strong>Profile-Guided Optimization</strong> — двухфазная сборка:</p>
  <ol id="PXh6">
    <li id="jzGD">Собираете «инструментированный» бинарник (<code>-fprofile-generate</code>), который сам собирает профиль выполнения.</li>
    <li id="wov8">Гоняете его на типичных нагрузках. Получаете файлы профилей: какие ветки как часто срабатывают, какие функции горячие.</li>
    <li id="Syjg">Пересобираете с <code>-fprofile-use=profiles/</code> — компилятор использует эти данные для решений.</li>
  </ol>
  <p id="1DEp">Что он оптимизирует с профилем? Размещение функций (горячие — рядом), порядок базовых блоков (ветка «с лайком» до ветки «без лайка»), inline-эвристики (если функция вызывается редко — не инлайнить, экономя icache), индирект-вызовы через виртуалки. Типовой прирост — 10-15% скорости. Clang сам собирается через PGO, Firefox собирается через PGO, браузеры Chromium — через PGO.</p>
  <p id="74Zu"><strong>AutoFDO</strong> (Google) — вариация без инструментирования. Собираете обычный бинарник, запускаете под sampling-профилировщиком (Linux <code>perf</code>), получаете профиль в другом формате, компилятор его тоже понимает. Главный плюс — профиль снимается с продакшена.</p>
  <h2 id="bolt-пост-линковочная-оптимизация">BOLT: пост-линковочная оптимизация</h2>
  <p id="UG5U">В 2016 году Facebook выкатывает <strong>BOLT</strong> — <em>Binary Optimization and Layout Tool</em>. Идея на грани фола: берём <strong>уже готовый бинарник</strong>, снимаем профиль <code>perf</code>, и пост-линковочно переставляем функции и базовые блоки внутри них по профилю. Никакой пересборки — только переупаковка готовых инструкций.</p>
  <p id="qhoP">Зачем? Кажется, что PGO делает то же самое. Но между PGO и BOLT есть существенная разница: PGO работает на уровне IR до генерации машинного кода. BOLT — на уровне готового машинного кода и может видеть вещи, которые недоступны компилятору (например, фактическую компоновку кода после линковки со всеми LTO и прочим).</p>
  <p id="4Zvf">Реальные результаты у Facebook: HHVM (runtime для Hack/PHP) после BOLT работает на 7-10% быстрее, чем то же самое с агрессивным PGO. Clang сам себя собирает через pipeline <code>LTO + PGO + BOLT</code>. Google стали использовать BOLT для ядра Linux на серверах — инструкционный icache hit rate вырос настолько, что это ощутимо по общему потреблению CPU в дата-центре.</p>
  <p id="yvKq">Минус BOLT в том, что он требует glibc определённой версии, стабильности формата, не работает на musl. И пользоваться им неудобно: один лишний шаг в CI. Но для топовой оптимизации серверного софта это безальтернативный инструмент.</p>
  <h1 id="часть-9-webassembly-линковка-родившаяся-в-браузере">Часть 9. WebAssembly: линковка, родившаяся в браузере</h1>
  <p id="nQUI">WASM — отдельная вселенная. Это не просто новая архитектура, здесь изначально другая философия работы с зависимостями.</p>
  <p id="IYZz">Каждый WASM-модуль — это <strong>таблица импортов и экспортов</strong>. Вместо «компилируй и линкуй» — «упакуй, а хост даст тебе нужные функции». Хостом может быть браузер (где функции — это JS и Web API) или WASI-рантайм (где они — ограниченный системный API).</p>
  <p id="MGEN"><code>wasm-ld</code> — часть LLD — делает следующее:</p>
  <ul id="Al78">
    <li id="rUMP">Собирает несколько объектных <code>.o</code>-файлов (WASM-объектов) в один <code>.wasm</code> модуль.</li>
    <li id="fKnn">Строит таблицы импортов и экспортов.</li>
    <li id="l2Nx">Обрабатывает релокации — у WASM есть свои типы, отличные от ELF.</li>
    <li id="ywp1">Поддерживает <code>--export</code>, <code>--import-memory</code>, <code>--shared-memory</code> для нестандартных сценариев.</li>
  </ul>
  <p id="gfQF">Интересная деталь: динамическая линковка в WASM не стандартизирована до конца. Emscripten придумал свою схему (<code>MAIN_MODULE</code>/<code>SIDE_MODULE</code>) с собственными PLT-аналогами поверх JS-glue. WASI Component Model (2024-2025) строит совсем другую модель — композицию компонентов через WIT-интерфейсы. Это уже не линковка в классическом смысле, это пакетное связывание модулей с явными контрактами типов — ближе к Protocol Buffers, чем к <code>.so</code>.</p>
  <p id="ZvOA">Rust, Go, Zig, C/C++ — все умеют компилироваться в WASM через <code>wasm32-unknown-unknown</code> или <code>wasm32-wasi</code> target. Nim — через <code>nimcache</code>, собирая C-бэкенд в WASM через Emscripten. Тема настолько большая, что заслуживает отдельной статьи.</p>
  <h1 id="часть-10-языки-и-их-линкеры">Часть 10. Языки и их линкеры</h1>
  <p id="6ktY">Каждый современный язык сделал свой выбор в этом большом зоопарке. Посмотрим на четырёх ключевых игроков.</p>
  <h2 id="go-собственный-линкер-по-наследству-от-plan-9">Go: собственный линкер по наследству от Plan 9</h2>
  <p id="tFUC">Go использует собственный линкер <code>cmd/link</code>, написанный на Go (с перегруппировкой после самобутстрапа в 2015). Это прямое наследие <strong>Plan 9</strong> — операционной системы Bell Labs. Над Plan 9 работали Роб Пайк, Кен Томпсон, Дэннис Ричи — те же имена, что и в Unix. В Plan 9 был свой компилятор <code>8c/6c/5c</code> и свой линкер <code>8l/6l/5l</code>. Когда Пайк и Томпсон создавали Go, они просто взяли код этого тулчейна и адаптировали. Это дало мгновенно работающий компилятор — а потом уже много лет выпрямляли и модернизировали.</p>
  <p id="hDtv">Уникальные преимущества своего линкера:</p>
  <ul id="CLiC">
    <li id="arnk"><strong>Кросс-компиляция без тулчейна.</strong> <code>GOOS=linux GOARCH=arm64 go build</code> на macOS — готов бинарник для Linux ARM64. Нет нужды в кросс-компиляторах, sysroot, glibc-headers-for-target. Go linker сам знает формат ELF, сам генерирует правильные структуры.</li>
    <li id="jyAm"><strong>Go-специфичные оптимизации.</strong> DCE на уровне отдельных символов (не функций, не файлов). Собственная генерация <code>.gopclntab</code> — таблицы для стектрейсов, рефлексии, сборщика мусора. Собственный DWARF-генератор.</li>
    <li id="lGpx"><strong>Полная статика по умолчанию.</strong> <code>CGO_ENABLED=0 go build</code> — и получаете один файл, работающий на пустом <code>FROM scratch</code> контейнере Docker. Это один из ключевых факторов успеха Go в облачной эпохе.</li>
    <li id="DCY4"><strong>Встроенная поддержка инъекции переменных.</strong> <code>go build -ldflags=&quot;-X main.version=$(git rev-parse HEAD) -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)&quot;</code> — и в бинарник вошиты версия и время сборки. Без костылей с conf-файлами.</li>
    <li id="5iD7"><strong>Воспроизводимые сборки.</strong> <code>-trimpath</code> убирает абсолютные пути, сборка побайтно идентична при одинаковом исходнике.</li>
    <li id="OaXQ"><strong>Уменьшение размера.</strong> <code>-ldflags=&quot;-s -w&quot;</code>: <code>-s</code> убирает таблицу символов, <code>-w</code> — DWARF-инфу. Бинарник уменьшается на 20-30%.</li>
    <li id="V77d"><strong>Плагины.</strong> <code>go build -buildmode=plugin</code> — собирает <code>.so</code>-файлы, которые можно загрузить в рантайме через <code>plugin.Open</code>. Работает только на Linux и macOS; на Windows плагины Go до сих пор не поддерживают.</li>
  </ul>
  <p id="fBem">Обратная сторона — <strong>скорость и качество кода</strong>. Линкер Go медленнее mold и lld. На монорепо Uber с десятками тысяч Go-пакетов линкер съедал гигабайты RAM и тратил минуты. Остин Клементс из Google <a href="https://docs.google.com/document/d/1D13QhciikbdLtaI67U6Ble5d_1nsI4befEd6_k1z91U/view" target="_blank">написал в 2019 году развёрнутый дизайн-док</a> о переписывании — в 2020-2021 годах линкер Go стал быстрее и экономнее по памяти в несколько раз, но до mold всё равно далеко. Плюс — Go не делает LTO в классическом смысле; оптимизации компилятора остаются внутри пакетов. На скорости runtime это сказывается против Rust/C++.</p>
  <p id="zo38">Если <code>CGO_ENABLED=1</code> (Go вызывает C-код через cgo), Go переключается в режим <strong>внешнего линкера</strong>: собирает весь Go-код в один <code>.o</code>, затем вызывает gcc/clang для финальной линковки. См. <a href="https://zharkevich.ru/blog/2026/linker/link_demo.go" target="_blank">link_demo.go</a>.</p>
  <h2 id="zig-линкер--кросс-компилятор--libc-всех-цветов">Zig: линкер + кросс-компилятор + libc всех цветов</h2>
  <p id="XW2d">Zig пошёл дальше Go. Он не просто имеет свой линкер, он включает полноценный кросс-тулчейн прямо в одном бинарнике <code>zig</code>:</p>
  <ul id="sNi1">
    <li id="ianA">Встроенный линкер на основе LLD (ELF, Mach-O, PE/COFF, WASM).</li>
    <li id="6kU6">Заголовочные файлы и исходники libc для <strong>40+ платформ</strong> — musl, glibc (различных версий), MinGW, Bionic (Android), mimicks macOS libSystem. Всё внутри дистрибутива.</li>
    <li id="gedw"><code>zig cc</code> — drop-in замена для gcc/clang с кросс-компиляцией: <code>zig cc -target x86_64-linux-musl hello.c</code>, <code>zig cc -target aarch64-macos -isysroot ...</code>.</li>
  </ul>
  <p id="Qiye">Это такой сдвиг парадигмы, что многие проекты на <strong>Rust</strong> (не Zig!) ставят <code>zig cc</code> в качестве линкера через <code>cargo-zigbuild</code> — именно ради кросс-компиляции без мучений с sysroot и glibc-versioning. Vagrant, разработчики CLI-инструментов, инди-игры — используют связку Rust + zig cc для выпуска бинарников под все платформы с одного ноутбука.</p>
  <p id="OgkT"><code>@cImport()</code> — ещё одна вещь, меняющая подход к FFI. Zig парсит <code>.h</code>-файлы во время компиляции и делает C-функции напрямую доступными в Zig. Не как в Rust, где нужен <code>bindgen</code>, генерирующий биндинги. Не как в Go, где cgo требует отдельной <code>//#include</code> магии. Zig читает <code>.h</code> и понимает его. См. <a href="https://zharkevich.ru/blog/2026/linker/link_demo.zig" target="_blank">link_demo.zig</a>.</p>
  <p id="RE6O">Инкрементальная компиляция — одна из ключевых долгосрочных целей Zig. Линкер проектируется так, чтобы перелинковывать только изменившиеся участки. На момент Zig 0.16 (2026 год) эта работа ещё не завершена полностью, но направление взято серьёзно.</p>
  <h2 id="rust-гибкость-и-арбитраж">Rust: гибкость и арбитраж</h2>
  <p id="eDQl">Rust по умолчанию использует системный линкер: GNU ld на Linux, Apple ld на macOS, <code>link.exe</code> (или <code>lld-link</code>) на Windows. Но любой шаг в сторону — и Cargo позволяет переопределить всё через <code>.cargo/config.toml</code>:</p>
  <pre id="7hfr">[target.x86_64-unknown-linux-gnu]
linker = &quot;clang&quot;
rustflags = [&quot;-C&quot;, &quot;link-arg=-fuse-ld=mold&quot;]

[target.aarch64-apple-darwin]
rustflags = [&quot;-C&quot;, &quot;link-arg=-fuse-ld=lld&quot;]

[target.x86_64-pc-windows-msvc]
linker = &quot;rust-lld.exe&quot;
</pre>
  <p id="0Qii">Переход с GNU ld на mold на крупных Rust-проектах (например, <code>rust-analyzer</code>, <code>bevy</code>, сам компилятор Rust) превращает минуты линковки в секунды. Это то изменение конфига, которое окупает себя в первые же часы после внедрения.</p>
  <p id="gBWm">Типы крейтов Rust — отдельная тема. Cargo умеет собирать:</p>
  <ul id="7bWR">
    <li id="wiTP"><code>bin</code> — обычный исполняемый файл.</li>
    <li id="Bfsu"><code>lib</code> / <code>rlib</code> — Rust-архив, линкуется только другими Rust-крейтами.</li>
    <li id="LNn4"><code>staticlib</code> — архив <code>.a</code> с C ABI, для статической линковки с C.</li>
    <li id="uNoL"><code>cdylib</code> — <code>.so</code>/<code>.dll</code>/<code>.dylib</code> с C ABI, для динамической линковки с C.</li>
    <li id="DyPh"><code>dylib</code> — <code>.so</code> с Rust ABI (не стабилизировано, используется внутри rustc).</li>
    <li id="Oemv"><code>proc-macro</code> — macro-крейт.</li>
  </ul>
  <p id="NB8L">Для FFI критичны <code>staticlib</code> и <code>cdylib</code>. Плюс атрибуты:</p>
  <pre id="kCxg">#[no_mangle]
pub extern &quot;C&quot; fn my_add(a: i32, b: i32) -&gt; i32 {
    a + b
}

#[link(name = &quot;ssl&quot;, kind = &quot;static&quot;)]
extern &quot;C&quot; {
    fn SSL_library_init() -&gt; i32;
}
</pre>
  <p id="oXNM">Для полной статики — target <code>x86_64-unknown-linux-musl</code>: линковка с musl вместо glibc, результат как у Go, один автономный файл без symbol versioning и других glibc-специфичных страданий. См. <a href="https://zharkevich.ru/blog/2026/linker/link_demo.rs" target="_blank">link_demo.rs</a>.</p>
  <h2 id="nim-высокоуровневый-синтаксис-с-честной-нативной-линковкой-через-c">Nim: высокоуровневый синтаксис с честной нативной линковкой через C</h2>
  <p id="QEWb">А теперь кое-что необычное. Nim не имеет собственного бэкенда. Он транслирует свой код в <strong>C</strong> (по умолчанию), C++ или Objective-C, и дальше эстафету принимает обычный C-компилятор и обычный системный линкер. Если у вас установлены <code>gcc</code> и <code>mold</code>, Nim будет использовать mold. Если вы на macOS — получите быстрый Apple ld-prime. На Windows — MSVC или MinGW по выбору.</p>
  <p id="Pxju">Это даёт Nim комбинацию свойств, которой нет у других «высокоуровневых» языков:</p>
  <p id="bQGS"><strong>1. FFI без биндингов вообще.</strong> Прагма <code>{.importc.}</code> объявляет символ внешним. <code>{.header.}</code> включает <code>.h</code> прямо в сгенерированный C-код. Никаких <code>bindgen</code>, никакой генерации врапперов, никаких отдельных шагов:</p>
  <pre id="IZs3">proc strlen(s: cstring): csize_t {.importc: &quot;strlen&quot;, header: &quot;&lt;string.h&gt;&quot;.}
echo strlen(&quot;hello, linker&quot;.cstring)  # 13
</pre>
  <p id="ex2L">Хотите использовать SQLite? <code>import sqlite3</code> или напрямую объявить пару прагм — и вы вызываете C-библиотеку как родные Nim-функции. Работает настолько нативно, что стирается граница между «C-код» и «Nim-код».</p>
  <p id="eIiY"><strong>2. Экспорт в линкер под нужным именем.</strong> <code>{.exportc.}</code> выставляет Nim-функцию как видимый C-символ без name mangling:</p>
  <pre id="1h8H">proc nimAdd(a, b: cint): cint {.exportc: &quot;nim_add&quot;, cdecl.} =
  a + b
</pre>
  <p id="qSro"><code>nm</code> покажет: <code>T nim_add</code>. Без всякого <code>_ZN3...E</code> — чистый C-символ, который может вызвать кто угодно. Это фундамент для того, чтобы писать Python-расширения, Node.js-модули или Nintendo Switch игры на Nim.</p>
  <p id="6PjY"><strong>3. Динамическая линковка на лету.</strong> Прагма <code>{.dynlib.}</code> — эквивалент <code>dlopen</code>/<code>LoadLibrary</code>, символ резолвится в момент первого вызова:</p>
  <pre id="HIGP">when defined(linux):   const libc = &quot;libc.so.6&quot;
elif defined(macosx):  const libc = &quot;libSystem.dylib&quot;
proc getpid(): cint {.importc, dynlib: libc.}

echo &quot;pid = &quot;, getpid()
</pre>
  <p id="XzcD"><strong>4. Встраивание чужого C-кода.</strong> Прагма <code>{.compile: &quot;vendor/foo.c&quot;.}</code> добавляет C-файл прямо в сборку; <code>{.emit: &quot;&quot;&quot; ... &quot;&quot;&quot;.}</code> позволяет встроить C-код буквально в Nim-файл. Граница между Nim и C — чисто синтаксическая, линкер видит один большой C-проект.</p>
  <p id="9s5M"><strong>5. Агрессивный dead code elimination.</strong> Nim при <code>-d:release</code> не тащит в бинарник всё, что вы импортировали — только реально используемые процедуры. Результат: hello world с полной стандартной библиотекой на macOS arm64:</p>
  <figure id="GSpo" class="m_retina">
    <img src="https://img3.teletype.in/files/69/a7/69a7544a-b09f-4f54-a282-23428659c957.png" width="790" />
  </figure>
  <p id="kGKR"><strong>6. Сборщик мусора на выбор — и каждый меняет рантайм.</strong></p>
  <ul id="lvsY">
    <li id="ZJ9P"><code>--mm:orc</code> (дефолт с 2022) — ARC + цикл-детектор. Детерминированное освобождение памяти, без stop-the-world пауз.</li>
    <li id="k0Rv"><code>--mm:arc</code> — чистый ARC без цикл-детектора. Максимальная скорость, если вы сами следите за циклическими ссылками.</li>
    <li id="dS9B"><code>--mm:none</code> — вообще никакого GC. Для embedded, ядер ОС, real-time систем.</li>
    <li id="yheB"><code>--mm:boehm</code> — классический boehm-gc для совместимости с C-библиотеками.</li>
  </ul>
  <p id="w4iR">Выбор <code>--mm</code> меняет то, какой рантайм линкуется в бинарник. С <code>--mm:none</code> получается нативный код уровня C с возможностями Nim — циклические операции с памятью придётся делать вручную.</p>
  <p id="YgTV"><strong>7. Полная статика через musl.</strong> Как и Rust с musl-target, одной строкой:</p>
  <pre id="lqjT">nim c -d:release --cc:musl-gcc --passC:-static --passL:-static app.nim
</pre>
  <p id="8rXo">Результат — один ELF-файл, запускающийся на любом Linux без зависимостей. Nim собирают так для Nintendo Switch, embedded-устройств, CLI-утилит, распространяемых как один файл.</p>
  <p id="kHLI">Вся живая демонстрация — в <a href="https://zharkevich.ru/blog/2026/linker/link_demo.nim" target="_blank">link_demo.nim</a>. Компилируется и запускается одной командой: <code>nim c -r link_demo.nim</code>.</p>
  <h1 id="часть-11-практические-рекомендации">Часть 11. Практические рекомендации</h1>
  <h2 id="для-ежедневной-разработки">Для ежедневной разработки</h2>
  <ul id="xi5E">
    <li id="pr5X"><strong>Rust + mold на Linux.</strong> <code>.cargo/config.toml</code> из примера выше. На средних проектах — сотни миллисекунд вместо секунд. На крупных (<code>rust-analyzer</code>, <code>bevy</code>) — минуты превращаются в секунды. Установка:<code>cargo install --locked cargo-binstall cargo binstall mold # или apt install mold / brew install mold </code></li>
    <li id="XzJP"><strong>Rust + sold на macOS.</strong> <code>sold</code> (mold для Mach-O) даёт похожий эффект. Или <code>lld</code> через Homebrew.</li>
    <li id="toUw"><strong>Инкрементальная компиляция.</strong> Rust и Nim имеют её по умолчанию. Go — частично. Zig — работа в процессе.</li>
    <li id="dshf"><strong>sccache</strong> — кеширование компиляции между сессиями. Не ускоряет линковку, но ускоряет всё до неё.</li>
  </ul>
  <h2 id="для-продакшн-сборок">Для продакшн-сборок</h2>
  <ul id="SlbD">
    <li id="WNqD"><strong>Rust.</strong> Thin LTO + <code>strip</code> + <code>codegen-units = 1</code>. Примерно 10-15% ускорения с 2-3-кратным увеличением времени сборки. Для критичных по скорости проектов — добавить Fat LTO и PGO.</li>
    <li id="0kK8"><strong>Go.</strong> <code>-ldflags=&quot;-s -w -X main.version=$(git describe --tags)&quot;</code>. Плюс <code>-trimpath</code> для воспроизводимых сборок.</li>
    <li id="YCns"><strong>Zig.</strong> По умолчанию <code>zig build -Doptimize=ReleaseFast</code> уже делает достаточно. Для минимального размера — <code>ReleaseSmall</code>.</li>
    <li id="S3X2"><strong>Nim.</strong> <code>nim c -d:release --opt:speed --passL:-s</code> или <code>--opt:size</code> в зависимости от целей.</li>
  </ul>
  <h2 id="для-docker-контейнеров">Для Docker-контейнеров</h2>
  <ul id="re5z">
    <li id="Aexu"><strong>Go.</strong> <code>CGO_ENABLED=0 GOOS=linux go build -o app .</code>, затем <code>FROM scratch</code> или <code>FROM gcr.io/distroless/static</code>. Финальный образ — единицы мегабайт.</li>
    <li id="aZky"><strong>Rust.</strong> Target <code>x86_64-unknown-linux-musl</code>, затем <code>FROM scratch</code> или <code>FROM alpine</code>. Gotcha: крейты с openssl-sys по умолчанию требуют system openssl; используйте <code>rustls</code> или feature <code>openssl/vendored</code>.</li>
    <li id="d8oC"><strong>Nim.</strong> <code>nim c -d:release --cc:musl-gcc --passL:-static</code> на Linux с установленным musl-gcc.</li>
    <li id="Lnc1"><strong>Zig.</strong> <code>zig build-exe -target x86_64-linux-musl main.zig -Doptimize=ReleaseSafe</code>.</li>
  </ul>
  <h2 id="для-кросс-компиляции">Для кросс-компиляции</h2>
  <ul id="eckD">
    <li id="RUaM"><strong>zig cc как линкер для всего.</strong> Rust через <code>cargo-zigbuild</code>, Go через <code>CC=&quot;zig cc -target x86_64-linux-musl&quot; CGO_ENABLED=1 go build</code>. Один ноутбук собирает под все платформы без настройки тулчейна.</li>
    <li id="KVUg"><strong>Docker с buildx.</strong> QEMU-based кросс-сборка, медленнее, но без настройки окружения.</li>
  </ul>
  <h2 id="для-отладки-и-анализа">Для отладки и анализа</h2>
  <p id="95sW">Инструменты, которые стоит иметь в арсенале:</p>
  <ul id="1gPj">
    <li id="iXk3"><strong><code>nm</code></strong> — список символов. <code>nm ./bin | grep &#x27; T &#x27;</code> — все экспортированные функции.</li>
    <li id="iDdo"><strong><code>objdump -d</code></strong> — дизассемблер. <code>objdump -d ./bin | less</code> — смотрим машинный код.</li>
    <li id="pesP"><strong><code>readelf -a</code></strong> — всё про ELF-файл: секции, сегменты, символы, релокации, динамическая секция.</li>
    <li id="PMVR"><strong><code>otool</code></strong> (macOS) — аналог для Mach-O: <code>otool -L ./bin</code> покажет зависимости.</li>
    <li id="STKQ"><strong><code>ldd</code></strong> (Linux) — какие <code>.so</code> подтянет программа при запуске. Осторожно: <code>ldd</code> фактически запускает программу, не используйте на недоверенных бинарниках.</li>
    <li id="vEyq"><strong><code>checksec</code></strong> — проверка защитных механизмов (RELRO, stack canaries, NX, PIE).</li>
    <li id="IylD"><strong><code>bloaty</code></strong> от Google — самый удобный анализатор «почему мой бинарник такой большой». Разделяет по секциям, по C++ namespace, по Rust crate.</li>
    <li id="VAj2"><strong><code>cargo bloat</code></strong> — аналог для Rust, выводит топ самых тяжёлых функций и крейтов.</li>
    <li id="uC4W"><strong><code>twiggy</code></strong> — bloaty для WASM.</li>
    <li id="A1cE"><strong><code>perf</code></strong> — Linux sampling profiler, для PGO и BOLT-профилей.</li>
  </ul>
  <h2 id="как-писать-код-дружелюбный-к-линкеру">Как писать код, дружелюбный к линкеру</h2>
  <ul id="YlxP">
    <li id="gbDq"><code>-fvisibility=hidden</code> в C/C++ — скрывайте всё по умолчанию, экспортируйте явно. Меньше символов, быстрее линковка, меньше attack surface для DLL hijacking.</li>
    <li id="qH2Z">В Rust — <code>pub(crate)</code> по умолчанию, <code>pub</code> только для API.</li>
    <li id="RD1I">В Nim — без <code>*</code> функция не экспортируется из модуля. Это по умолчанию; код у вас дружелюбен к линкеру.</li>
    <li id="EBr4">В Go — маленькая буква в начале = приватный. Опять по умолчанию.</li>
    <li id="b1Uc">Для больших C/C++ проектов — <code>-ffunction-sections -fdata-sections -Wl,--gc-sections</code>. 10-30% уменьшения размера.</li>
  </ul>
  <h1 id="эпилог">Эпилог</h1>
  <p id="Tkur">Линкер — это невидимый диктатор всей индустрии. От него зависит почти всё, что вам важно:</p>
  <ul id="5M4n">
    <li id="BF5g">Автономен ли ваш бинарник или требует набор библиотек.</li>
    <li id="9hp8">Сколько секунд уходит на каждое «собрать и запустить».</li>
    <li id="SFxH">Возможна ли кросс-компиляция без настройки тулчейна.</li>
    <li id="x2Yi">Работает ли Docker-образ размером 5 МБ вместо 800 МБ.</li>
    <li id="mbPE">Как быстро стартует программа после двойного клика.</li>
    <li id="PKOl">Насколько защищён бинарник от ROP-атак и подмены GOT.</li>
    <li id="ZA7O">Запустится ли ваша программа, собранная сегодня, через двадцать лет.</li>
  </ul>
  <p id="6aLd">Go, Zig, Rust и Nim сделали разные архитектурные выборы в этой точке. Go — полностью свой линкер ради кросс-компиляции и статики. Zig — LLD плюс встроенный тулчейн на все случаи жизни. Rust — гибкий выбор системного линкера с мощной системой типов крейтов. Nim — честное партнёрство с C-инфраструктурой через importc/exportc/dynlib. Каждый выбор отражает глубинную философию языка.</p>
  <p id="G4F2">А параллельно тихо продолжается гонка: Руи Уэяма в одиночку сделал mold и сдвинул планку в 25 раз. Дэвид Латтимор в Meta пишет Wild и пытается сделать то же самое с инкрементальной линковкой. Apple втихую переписывает <code>ld</code> в Xcode. BOLT от Facebook выжимает дополнительные 10% скорости у уже собранных бинарников. WebAssembly строит совсем другую модель компонентов.</p>
  <p id="wLZ3">За каждой строчкой «Linking…» в статус-баре вашей IDE — семьдесят лет инженерной эволюции, начавшейся в Кембридже в 1949 году с перфоленты и двадцатилетнего аспиранта по имени Дэвид Уилер.</p>
  <p id="xJzL">Следующий раз, когда будете жаловаться на медленную сборку — вспомните, что именно линкер, а не компилятор, скорее всего, этому виной. И знайте: есть mold.</p>
  <h1 id="примеры-кода">Примеры кода</h1>
  <ul id="7Kl9">
    <li id="fITW"><a href="https://zharkevich.ru/blog/2026/linker/link_demo.nim" target="_blank">link_demo.nim</a> — FFI через <code>{.importc.}</code>, экспорт через <code>{.exportc.}</code>, динамическая линковка <code>{.dynlib.}</code>, dead code elimination, выбор GC.</li>
    <li id="sOGT"><a href="https://zharkevich.ru/blog/2026/linker/link_demo.rs" target="_blank">link_demo.rs</a> — name mangling, LTO, настройка линкера через <code>.cargo/config.toml</code>, musl-target для полной статики.</li>
    <li id="4R1U"><a href="https://zharkevich.ru/blog/2026/linker/link_demo.go" target="_blank">link_demo.go</a> — CGO-переключатель, статическая линковка по умолчанию, внутреннее устройство Go-линкера.</li>
    <li id="jIpX"><a href="https://zharkevich.ru/blog/2026/linker/link_demo.zig" target="_blank">link_demo.zig</a> — кросс-компиляция без тулчейна, <code>zig cc</code>, <code>@cImport()</code> для прямого импорта C-заголовков.</li>
  </ul>
  <h1 id="источники">Источники</h1>
  <ul id="tK6I">
    <li id="f6mU"><a href="https://www.iecc.com/linker/" target="_blank">Linkers and Loaders (John R. Levine, 1999)</a> — классическая книга, до сих пор лучший учебник по теме.</li>
    <li id="BIdY"><a href="https://www.akkadia.org/drepper/dsohowto.pdf" target="_blank">How to Write Shared Libraries (Ulrich Drepper, 2011)</a> — техническая глубина про PLT, GOT, lazy binding и RELRO от одного из мейнтейнеров glibc.</li>
    <li id="JlnF"><a href="https://github.com/rui314/mold" target="_blank">mold: A Modern Linker (Rui Ueyama)</a> — исходники и дизайн-документы mold.</li>
    <li id="RTcM"><a href="https://lld.llvm.org/" target="_blank">LLD — The LLVM Linker</a> — документация LLD.</li>
    <li id="gmQs"><a href="https://docs.google.com/document/d/1D13QhciikbdLtaI67U6Ble5d_1nsI4befEd6_k1z91U/view" target="_blank">Building a Better Go Linker (Austin Clements, 2019)</a> — план переработки линкера Go.</li>
    <li id="JyQV"><a href="https://www.uber.com/blog/fixing-gos-linker/" target="_blank">Fixing Go’s Linker (Uber Engineering)</a> — как Uber боролся с медленной линковкой на своей монорепо.</li>
    <li id="iDya"><a href="https://internals-for-interns.com/posts/the-go-linker/" target="_blank">Understanding the Go Compiler: The Linker</a> — устройство линкера Go изнутри.</li>
    <li id="lSpB"><a href="https://ziglang.org/documentation/master/" target="_blank">Zig Build System</a> — документация Zig.</li>
    <li id="BULq"><a href="https://nim-lang.org/docs/manual.html#foreign-function-interface" target="_blank">Nim Manual: Foreign Function Interface</a> — importc/exportc/dynlib в Nim.</li>
    <li id="hdTl"><a href="https://en.wikipedia.org/wiki/DLL_Hell" target="_blank">DLL Hell (Wikipedia)</a> — история проблемы.</li>
    <li id="62VP"><a href="https://github.com/davidlattimore/wild" target="_blank">Wild Linker</a> — экспериментальный инкрементальный линкер на Rust.</li>
    <li id="Hsvv"><a href="https://llvm.org/devmtg/2017-10/slides/Ueyama-lld.pdf" target="_blank">lld: A Fast, Simple and Portable Linker (Rui Ueyama, LLVM Dev Meeting 2017)</a> — слайды о создании LLD.</li>
    <li id="oGQY"><a href="https://clang.llvm.org/docs/ThinLTO.html" target="_blank">ThinLTO: Scalable and Incremental LTO</a> — документация Thin LTO.</li>
    <li id="aJiR"><a href="https://arxiv.org/abs/1807.06735" target="_blank">BOLT: A Practical Binary Optimizer for Data Centers and Beyond (Panchenko et al., 2019)</a> — научная статья про BOLT.</li>
    <li id="fv3s"><a href="https://en.wikipedia.org/wiki/David_Wheeler_(computer_scientist)" target="_blank">David Wheeler and the Birth of Subroutines</a> — история изобретения подпрограммы и линковки.</li>
    <li id="P2eM"><a href="https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/" target="_blank">The 101 of ELF Binaries on Linux</a> — практическое введение в ELF.</li>
    <li id="zx6q"><a href="https://component-model.bytecodealliance.org/" target="_blank">WebAssembly Component Model</a> — будущее модульности в WASM.</li>
  </ul>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/time-is-hard</guid><link>https://azhark.cc/time-is-hard?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/time-is-hard?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Время — самый сложный тип данных</title><pubDate>Fri, 17 Apr 2026 19:34:20 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a8/c0/a8c0f216-e32a-4275-9d49-9624a225a260.png"></media:content><description><![CDATA[<img src="https://img1.teletype.in/files/8f/ae/8faeff6c-4589-4c90-b399-986d19a1c7b8.png"></img>Представьте: канун Нового 2017 года. Инфраструктура Cloudflare обслуживает миллионы DNS-запросов в секунду — миллиарды людей не подозревают, что где-то в стоечных залах по всему миру в этот момент начинается маленькая катастрофа.]]></description><content:encoded><![CDATA[
  <figure id="iZDo" class="m_original">
    <img src="https://img1.teletype.in/files/8f/ae/8faeff6c-4589-4c90-b399-986d19a1c7b8.png" width="1672" />
  </figure>
  <p id="dRRH">Представьте: канун Нового 2017 года. Инфраструктура Cloudflare обслуживает миллионы DNS-запросов в секунду — миллиарды людей не подозревают, что где-то в стоечных залах по всему миру в этот момент начинается маленькая катастрофа.</p>
  <p id="LQ3d">Ровно в полночь по UTC официальные хранители мирового времени добавили к нему ещё одну секунду. Лишнюю. Поверх обычных 86 400. В этот момент системные часы на серверах Cloudflare сделали шаг назад — и одна строчка кода на Go не смогла это пережить.</p>
  <p id="BhMJ">Строчка вычитала два вызова <code>time.Now()</code>, чтобы узнать длительность интервала. Длительность внезапно оказалась отрицательной. Отрицательное число попало в <code>rand.Int63n()</code>, которая умеет работать только с положительными значениями. DNS-резолвер запаниковал и упал.</p>
  <p id="r3cE">Инцидент затронул около 0,2 % DNS-запросов в 102 дата-центрах и длился почти семь часов. Исправление в коде заняло один символ — замену <code>== 0</code> на <code>&lt;= 0</code>. Но чтобы этот символ появился на своём месте, команде пришлось признать неудобную истину: <strong>время — не число</strong>. За кажущейся простотой <code>Date.now()</code> скрываются часовые пояса, меняющиеся по политическим решениям, секунды, которые длиннее других секунд, дни, которых не было, и часы, которые идут назад.</p>
  <p id="f8TF">Эта статья — экскурсия по подземельям этой абстракции. С историей, примерами кода на четырёх языках и парой предупреждений на будущее.</p>
  <h2 id="как-человечество-училось-измерять-время">Как человечество училось измерять время</h2>
  <p id="sikL">До середины XIX века у каждого города были свои часы. Ярославль жил по ярославскому полудню, Томск — по томскому. Всё изменили железные дороги. Без единого расписания поезда сталкивались — буквально, с лязгом металла и человеческими жертвами. Нужно было договариваться.</p>
  <p id="6093">В 1884 году в Вашингтоне прошла Международная меридианная конференция. Она назначила Гринвичский меридиан нулевым, разделила Землю на часовые пояса и впервые заставила всё цивилизованное человечество согласиться хотя бы в одном: <strong>который сейчас час</strong>.</p>
  <p id="9oIe">В 1967 году физики снова всё переиграли: секунду определили не как долю солнечных суток, а как <strong>9 192 631 770</strong> колебаний атома цезия-133. Атомные часы точнее, чем вращение планеты. И тут выяснилось страшное: Земля — не идеальный хронометр.</p>
  <p id="QM18">Наша планета замедляется из-за приливного трения Луны, ускоряется, когда жидкое ядро перераспределяет массу, и плывёт по времени в свою сторону. Астрономическое время (UT1) и атомное (TAI) постепенно расходятся. Атомная секунда стабильна, а вот секунда «настоящая» — нет.</p>
  <p id="b9kK">В 1972 году придумали компромисс: UTC (<em>Coordinated Universal Time</em>) и механизм <strong>високосных секунд</strong> (<em>leap seconds</em>). Когда UT1 начинает отставать от UTC почти на одну секунду, в UTC добавляется дополнительная — чтобы не терять связь с солнечным днём. С 1972 года таких секунд было добавлено 27, все со знаком плюс. Последняя — 31 декабря 2016 года. Сегодня UTC отстаёт от TAI на 37 секунд. И это отставание — прямое наследство кривизны небесной механики.</p>
  <h2 id="високосные-секунды-заплатка-которую-решили-отменить">Високосные секунды: заплатка, которую решили отменить</h2>
  <p id="Rl3H">Механизм выглядит безобидно: одна лишняя секунда раз в пару лет, кому это может помешать? Но оказалось, что всем.</p>
  <p id="vuac">В момент вставки минута длится не 60 секунд, а 61: после 23:59:59 идёт 23:59:60, а только потом 00:00:00. Большинство программ никогда не видели «60»-ю секунду. Они её и не ждали.</p>
  <p id="C26M">30 июня 2012 года баг в ядре Linux заставил потоки крутиться в бесконечном цикле, выжигая процессор на 100 %. Упали Reddit (полностью), Qantas Airlines (отказ системы бронирования — пассажиры застряли в аэропортах), Mozilla, LinkedIn, Yelp, FourSquare. Cloudflare в 2017 году стал продолжением той же истории, просто на другом языке программирования и в другом дата-центре.</p>
  <p id="g1np">В ноябре 2022 года 27-я Генеральная конференция по мерам и весам (CGPM) приняла историческое решение: отменить високосные секунды к 2035 году (возможно — раньше). Допустимую разницу между UT1 и UTC увеличат, позволят ей накапливаться десятилетиями. Какой именно станет новая «допустимая» величина — решит 28-я CGPM в 2026 году. Возможно, минута. Возможно, больше.</p>
  <p id="VVDd">Крупные облачные провайдеры ждать не стали. С 2008 года Google использует так называемый leap smearing — «размазывание» високосной секунды на 24 часа: чуть замедляет или ускоряет все свои часы, и момент вставки проходит незаметно. Amazon и Meta применяют тот же приём. Но у этой хитрости есть побочный эффект: часы Google и часы биржи NYSE расходятся на доли секунды — что при высокочастотной торговле, где миллисекунда решает судьбу миллиона долларов, может иметь последствия.</p>
  <h3 id="климат-и-секунда-которой-ещё-не-было">Климат и секунда, которой ещё не было</h3>
  <p id="AVhE">В марте 2024 года в журнале <em>Nature</em> появилась статья Дункана Агню из Института океанографии Скриппса с выводом, которого никто не ожидал. Таяние ледников Гренландии и Антарктиды перераспределяет массу планеты: вода стекает от полюсов к экватору, и Земля, как фигурист, разводящий руки, начинает вращаться медленнее.</p>
  <p id="iQ7r">Следствие: без климатических изменений первую в истории отрицательную високосную секунду пришлось бы вставлять уже к 2026 году. Отрицательную — значит, пропустить одну секунду, пройти от 23:59:58 сразу к 00:00:00. Таяние льдов отсрочило этот момент примерно до 2029-го.</p>
  <p id="IXZx">Отрицательную високосную секунду никогда не тестировали. Meta предупредила, что это может оказать «разрушительное влияние» на цифровую инфраструктуру просто потому, что никто не знает, как поведут себя триллионы строк кода, написанного в расчёте на то, что время всегда идёт вперёд. Отличный аргумент, чтобы наконец похоронить всю систему.</p>
  <h2 id="день-которого-не-было">День, которого не было</h2>
  <p id="FkD0">29 декабря 2011 года, четверг, в Самоа завершился как обычно. Следующим днём стала суббота, 31 декабря. Пятница, 30 декабря, в Самоа не существовала.</p>
  <p id="RSYP">Причина — экономическая. Главные торговые партнёры островного государства сдвинулись с США в сторону Австралии и Новой Зеландии. А Самоа оставалась на американской стороне линии перемены дат (UTC-11) и теряла два рабочих дня в неделю: когда в Сиднее понедельник, в Апиа ещё воскресенье. Перепрыгнув на UTC+13, страна синхронизировалась с партнёрами и стала одной из первых на планете встречать Новый год. Американское Самоа — всего в ста километрах — осталось на UTC-11. Разница между соседними островами составила <strong>25 часов</strong>.</p>
  <p id="7UMo">Тем, у кого на несуществующую пятницу была назначена смена, всё равно выплатили зарплату как за полный рабочий день. Банки специально оговорили: проценты за пропущенный день никто не начислит и не спишет.</p>
  <p id="JLqI">И это был не первый такой прыжок. 4 июля 1892 года Самоа перепрыгнула в обратную сторону, чтобы синхронизироваться с тогдашним партнёром — США. В тот раз 4 июля праздновали дважды. В 1844 году Филиппины (тогда испанская колония) пропустили 31 декабря, перейдя с американского календарного дня на азиатский. В 1995 году Кирибати сдвинула линию перемены дат и создала пояс UTC+14 — самый крайний в мире.</p>
  <p id="0D2Q">Следствие, о котором редко думают: <strong>в любой момент на Земле одновременно существуют три разных календарных дня</strong>. Два часа в сутки — ровно так. Подумайте об этом, когда в следующий раз будете писать <code>new Date().toDateString()</code>.</p>
  <h2 id="38-а-не-24">38, а не 24</h2>
  <p id="0MRZ">Распространённое заблуждение: на Земле 24 часовых пояса. На самом деле — примерно 38, от UTC-12 до UTC+14. И не все смещения — целые часы. Индия живёт по UTC+5:30, Непал — по UTC+5:45, острова Чатем в Новой Зеландии — по UTC+12:45. Последнее означает, что перевод встречи «на 15 минут вперёд» в Чатеме может случайно прыгнуть через границу часового пояса.</p>
  <p id="ZS1z">Часовые пояса меняются постоянно. Это политика, а не физика. Вот беглый взгляд на последние годы из базы IANA (tzdata):</p>
  <figure id="fJi4" class="m_retina">
    <img src="https://img4.teletype.in/files/be/14/be1429d4-2cf9-4bc9-a1ee-4a08fbe5a043.png" width="781" />
  </figure>
  <p id="m9MJ">База IANA обновляется несколько раз в год. Если ваше приложение работает с часовыми поясами и вы не обновляете tzdata — вы прямо сейчас показываете неправильное время хотя бы одной категории пользователей. Прямо сейчас, пока вы читаете этот абзац.</p>
  <h2 id="летнее-время-часы-призраки-и-часы-двойники">Летнее время: часы-призраки и часы-двойники</h2>
  <p id="Tb2L">При переходе на летнее время весной часы перескакивают с 1:59 на 3:00. Час с 2:00 до 3:00 не существует — любое время в этом промежутке невалидно. Осенью часы возвращаются назад, и час с 1:00 до 2:00 повторяется дважды — одно и то же «1:30» становится неоднозначным: до перевода или после?</p>
  <p id="FBZT">Звучит как забавный курьёз. А вот истории, которые реальны:</p>
  <ul id="3C9F">
    <li id="Kd16">В больницах медсёстры знают: при осеннем переводе часов записи жизненных показателей за «первый» час с 1:00 до 2:00 могут быть удалены информационной системой, когда часы вернутся к 1:00 и начнут писать поверх. Некоторые клиники на время перехода переключаются на бумажные журналы;</li>
    <li id="n3rJ">9 августа 2022 года правительство Чили объявило, что летнее время начнётся 10 сентября вместо 4 сентября — с уведомлением за месяц. Microsoft Teams и Outlook сдвинули встречи на час не туда, аутентификация Kerberos сломалась из-за несоответствия временных меток;</li>
    <li id="a9DS">В 2005 году США расширили DST на четыре недели. Затраты на патчи и тестирование оценили в 350 миллионов долларов. Такой оказалась стоимость одной строчки в законе.</li>
  </ul>
  <h2 id="два-типа-часов-которые-все-путают">Два типа часов, которые все путают</h2>
  <p id="Ns8X">Операционная система даёт программисту два совершенно разных механизма измерения времени. Смешение их — источник большинства таймерных багов на свете.</p>
  <p id="q8KH"><strong>Wall clock</strong> (часы реального мира, <code>CLOCK_REALTIME</code>) — показывают текущую дату и время. Синхронизируются через NTP, могут быть переведены вручную, переживают високосные секунды и DST. <strong>Могут прыгать вперёд и назад в любой момент</strong>.</p>
  <p id="O4d0"><strong>Monotonic clock</strong> (монотонные часы, <code>CLOCK_MONOTONIC</code>) — считают наносекунды с некоторого произвольного момента (обычно — с загрузки системы). <strong>Никогда не идут назад</strong>. Не привязаны к реальной дате. Бессмысленны после перезагрузки или между разными процессами — это просто счётчик, свой для каждой живой системы.</p>
  <p id="fpgr">Разница между этими двумя механизмами — не вопрос эстетики, а ответ на два разных вопроса. Wall clock отвечает на «<strong>когда</strong> это случилось?» — в такую-то дату, в таком-то часовом поясе. Monotonic — на «<strong>сколько прошло</strong> между двумя точками?». Одно и то же число не может быть одновременно ответом на оба: момент можно подкрутить через NTP, длительность — нет. Отсюда и правило, за нарушение которого платят production-инциденты:</p>
  <ul id="bgML">
    <li id="6wdj"><strong>измерение длительности</strong> (латентность, таймауты, бенчмарки) — <strong>монотонные часы</strong>;</li>
    <li id="v2ZW"><strong>запись момента события</strong> (логи, временные метки, даты файлов) — <strong>wall clock</strong>.</li>
  </ul>
  <p id="XGaW">Именно это правило Cloudflare и нарушила в 2017 году — не по глупости, а из-за языка. Go до версии 1.9 просто не предоставлял монотонных часов: <code>time.Now()</code> возвращал только wall clock, больше взять было негде. После инцидента Расс Кокс предложил и реализовал встраивание монотонного значения прямо в тип <code>time.Time</code> — и начиная с Go 1.9 каждый вызов <code>time.Now()</code> запоминает оба значения одновременно.</p>
  <p id="aQ5F">И вот тут становится интересно. Потому что теоретическое знание «для длительности нужны монотонные часы» не спасает никого. Спасает стандартная библиотека, в которой сделать неправильно либо трудно, либо вообще невозможно. Cloudflare научила этому весь мир на своих серверах — и языки ответили на урок по-разному.</p>
  <h2 id="четыре-языка-четыре-философии">Четыре языка, четыре философии</h2>
  <p id="1JUR">Возьмём одну и ту же небольшую задачу — измерить длительность вычисления, записать момент события, поспать миллисекунду, разобрать Unix timestamp — и посмотрим, как её решают четыре языка с четырьмя разными представлениями о том, что значит «удобно».</p>
  <p id="s9Y9">Исходники целиком: <a href="https://zharkevich.ru/blog/2026/time-is-hard/time_demo.rs" target="_blank">Rust</a> · <a href="https://zharkevich.ru/blog/2026/time-is-hard/time_demo.go" target="_blank">Go</a> · <a href="https://zharkevich.ru/blog/2026/time-is-hard/time_demo.zig" target="_blank">Zig</a> · <a href="https://zharkevich.ru/blog/2026/time-is-hard/time_demo.nim" target="_blank">Nim</a>.</p>
  <h3 id="rust-два-типа--и-точка">Rust: два типа — и точка</h3>
  <p id="LXbd">Rust подошёл к проблеме радикально: просто сделал монотонные и реальные часы <strong>несовместимыми типами</strong>. Компилятор не даст их перепутать.</p>
  <pre id="adgv">use std::time::{Instant, SystemTime};

// Монотонные часы — для измерения длительности
let start = Instant::now();
// ... работа ...
let elapsed = start.elapsed(); // Duration, всегда &gt;= 0

// Wall clock — для записи момента
let now = SystemTime::now();
let since_epoch = now.duration_since(SystemTime::UNIX_EPOCH); // Result!
</pre>
  <p id="skko"><code>Instant::elapsed()</code> возвращает <code>Duration</code>, гарантированно неотрицательную. <code>SystemTime::elapsed()</code> возвращает <code>Result&lt;Duration, SystemTimeError&gt;</code> — компилятор <em>заставляет</em> обработать случай, когда часы пошли назад. Даже если программист этого не хочет. Особенно если не хочет.</p>
  <p id="3b1x"><code>Instant</code> при этом нельзя сериализовать: монотонное время не имеет смысла за пределами процесса. Встроенного <code>DateTime</code> в std нет — для календарных операций используют крейты <code>chrono</code> или <code>time</code>.</p>
  <h3 id="go-один-тип-два-механизма-внутри">Go: один тип, два механизма внутри</h3>
  <p id="Sbee">Go пошёл другим путём: оставил единственный тип <code>time.Time</code>, но спрятал внутри него сразу оба часовых механизма.</p>
  <pre id="3a9r">start := time.Now() // содержит wall clock + monotonic reading
// ... работа ...
elapsed := time.Since(start) // использует monotonic, игнорирует wall
</pre>
  <p id="1gEU">При вычислении разницы (<code>Sub</code>, <code>Since</code>, <code>Until</code>) Go автоматически берёт монотонное значение, если оно есть у обоих операндов. При сериализации (<code>MarshalJSON</code>, <code>Format</code>) монотонное значение отбрасывается. То же при календарных операциях (<code>AddDate</code>, <code>Round</code>).</p>
  <p id="pdrY">Опрос реальных проектов показал: около <strong>30 %</strong> вызовов <code>time.Now()</code> в коде на Go измеряют длительность. Дизайн Go делает все эти вызовы корректными автоматически, без изменения публичного API.</p>
  <p id="GESE">У этой элегантности есть ловушки. <code>time.Parse()</code> возвращает <code>Time</code> <strong>без</strong> монотонного значения — если потом вычислить <code>time.Since(parsedTime)</code>, Go откатится к wall clock, и результат может оказаться отрицательным. Ещё одна мина: <code>==</code> и <code>Equal()</code> для <code>time.Time</code> делают разные вещи:</p>
  <pre id="FzlS">t1 := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2025, 1, 1, 0, 0, 0, 0, london) // Europe/London
t1 == t2     // false — разные Location в структуре
t1.Equal(t2) // true  — одно и то же мгновение
</pre>
  <p id="jxx7">И, наконец, легендарная дата-образец Go. Вместо <code>YYYY-MM-DD HH:mm:ss</code> используется магическое <strong>Mon Jan 2 15:04:05 MST 2006</strong>, в котором все элементы — последовательные числа: 01/02 03:04:05 ‘06 −0700. Элегантно и совершенно непривычно.</p>
  <h3 id="zig-прозрачность-и-контроль">Zig: прозрачность и контроль</h3>
  <p id="LOHf">Несколько дней назад, 5 апреля 2026 года, вышла Zig 0.16.0 — с серьёзно перекроенной стандартной библиотекой. Теперь всё, что связано с вводом-выводом и временем, проходит через новый интерфейс <code>std.Io</code>, который передаётся в <code>main</code> первым параметром. Прежний <code>std.time.Timer</code>, <code>std.time.sleep</code> и <code>std.fs.File.stdout()</code> исчезли.</p>
  <pre id="myQ7">const std = @import(&quot;std&quot;);

pub fn main(init: std.process.Init) !void {
    const io = init.io;

    // Монотонные часы (CLOCK_MONOTONIC)
    const start = std.Io.Clock.Timestamp.now(io, .awake);
    // ... работа ...
    const elapsed = start.untilNow(io); // Duration, &gt;= 0

    // Wall clock (Unix epoch)
    const wall = std.Io.Clock.Timestamp.now(io, .real);
    const seconds = wall.raw.toSeconds();    // i64
    const nanos   = wall.raw.toNanoseconds(); // i96

    // Sleep — возвращает Cancelable!void
    try std.Io.sleep(io, std.Io.Duration.fromMilliseconds(1), .awake);
}
</pre>
  <p id="2iaT">В Zig часы — это набор именованных источников: <code>.awake</code> (монотонный, не тикает во время сна системы), <code>.boot</code> (монотонный, тикает всегда), <code>.real</code> (wall clock), <code>.cpu_process</code>, <code>.cpu_thread</code>. Встроенного <code>DateTime</code> нет, но <code>std.time.epoch.EpochSeconds</code> позволяет разложить Unix timestamp на год, месяц, день и секунды. Часовые пояса — только через внешние библиотеки.</p>
  <p id="Kef1">Плюс такого подхода — полная прозрачность: никакой магии, понятно, какой источник часов используется. Минус — весь этот контроль лежит на программисте.</p>
  <h3 id="nim-datetime-в-стандартной-библиотеке">Nim: DateTime в стандартной библиотеке</h3>
  <p id="1GDJ">Nim — самый «высокоуровневый» из четырёх. Часовые пояса, парсинг, форматирование и календарная арифметика встроены в стандартную библиотеку, а монотонные часы отделены в собственный модуль.</p>
  <pre id="lqSM">import std/[times, monotimes, os]

# Монотонные часы — для измерения длительности
let start = getMonoTime()
# ... работа ...
let elapsed = getMonoTime() - start  # Duration, &gt;= 0
echo elapsed.inNanoseconds

# Wall clock и календарь
let now = getTime()
echo now.toUnix        # int64 секунд
echo now.utc           # DateTime в UTC
echo now.local         # DateTime в локальной зоне

# Парсинг и часовые пояса
let m = dateTime(2038, mJan, 19, 3, 14, 7, zone = utc())
echo m.format(&quot;yyyy-MM-dd HH:mm:ss zzz&quot;)
</pre>
  <p id="jU21"><code>Time</code> в Nim хранится как <code>(seconds: int64, nanosecond: NanosecondRange)</code> — Y2038 не угрожает. <code>DateTime</code> содержит внутри объект <code>Timezone</code> и потому знает, в каком поясе был создан момент. <code>MonoTime</code> из <code>std/monotimes</code> — обёртка над <code>CLOCK_MONOTONIC</code>, не зависит от NTP и никогда не идёт назад.</p>
  <p id="08Uy">Главная ловушка тут — та же, что у Go: при желании можно вычесть два <code>Time</code>, полученных в разные моменты через <code>getTime()</code>, и получить странный результат, если часы перед этим подкрутил NTP. Правильный путь — <code>getMonoTime()</code>.</p>
  <h3 id="сравнение">Сравнение</h3>
  <figure id="RpqX" class="m_retina">
    <img src="https://img3.teletype.in/files/6b/d0/6bd00fd2-43da-4249-85fd-836030df0728.png" width="783" />
  </figure>
  <p id="MQ9u">Четыре языка — четыре позиции на спектре: от Rust (максимум защиты, минимум удобства из коробки) до Nim (максимум удобства, аккуратность на совести программиста). Go и Zig — между ними, каждый по-своему.</p>
  <h2 id="y2038-32-битный-апокалипсис">Y2038: 32-битный апокалипсис</h2>
  <p id="kvDf">19 января 2038 года, 03:14:07 UTC — момент, когда 32-битный знаковый Unix timestamp (<code>int32</code>) переполнится. Число <code>2 147 483 647</code> прыгнет в <code>−2 147 483 648</code>, и устройства, живущие в таких часах, внезапно окажутся в декабре 1901 года.</p>
  <p id="BwkF">Это не гипотеза. Вот что случалось с 32-битными счётчиками раньше:</p>
  <ul id="ooP0">
    <li id="NtVh"><strong>Boeing 787</strong> (2015). Федеральное авиационное управление США выпустило директиву о лётной годности: блоки управления генераторами входили в аварийный режим после <strong>248 дней</strong> непрерывной работы. Причина — переполнение счётчика сотых долей секунды в <code>int32</code> (2³¹/100/86400 ≈ 248 дней). Временное решение: перезагружать самолёт минимум раз в 248 дней;</li>
    <li id="8mfU"><strong>Зонд Deep Impact</strong> (2013). После успешной миссии по комете Темпеля зонд продолжал работать ещё восемь лет — пока 32-битный счётчик десятых долей секунды не переполнился (~13,5 лет). Связь с Землёй пропала навсегда;</li>
    <li id="CuyR"><strong>Microsoft Zune</strong> (31 декабря 2008). Тысячи плееров зависли одновременно: прошивка не умела обрабатывать 366-й день високосного года и уходила в бесконечный цикл. Решение от Microsoft звучало издевательски: «подождите до 1 января».</li>
  </ul>
  <p id="TIO5">Софт давно отреагировал. В ядре Linux с 2020 года <code>time_t</code> — 64-битный даже на 32-битных архитектурах. Стандартные библиотеки современных языков хранят время с огромным запасом: Rust — секунды в <code>u64</code>, Go — наносекунды в <code>int64</code> (до 2262 года), Zig — <code>i96</code> наносекунд, Nim — <code>int64</code> секунд. Чтобы написать сегодня приложение, которое сломается 19 января 2038, нужно постараться.</p>
  <p id="TLJ6">Проблема в другом — в железе и прошивках, которые написаны «на двадцать лет вперёд, и чтобы не трогали». 32-битные микроконтроллеры в домашних маршрутизаторах, промышленных контроллерах на заводах, автомобильной электронике и медицинских приборах останутся в строю ещё десятилетия. Их прошивки часто используют 32-битный <code>time_t</code> просто потому, что у CPU нет 64-битной арифметики, а обновления на них давно никто не выпускает. Для этого класса устройств 19 января 2038 года — не абстракция, а вполне конкретная дата, после которой часть из них просто перестанет корректно работать.</p>
  <p id="U8sr">И чтобы жизнь не казалась простой: <strong>в ноябре 2038 года</strong> произойдёт очередной GPS Week Number Rollover для устройств с 10-битным счётчиком недель. Два переполнения в одном году.</p>
  <h2 id="почему-sleep1-не-спит-ровно-секунду">Почему <code>sleep(1)</code> не спит ровно секунду</h2>
  <p id="AhXf">Сама функция <code>sleep()</code> (и её аналоги во всех языках) не гарантирует точную задержку. Она гарантирует <strong>не менее</strong> запрошенного времени. Операционная система может разбудить процесс позже — если процессор занят, если планировщик решил иначе, если произошло прерывание.</p>
  <p id="KWrp">Демонстрация прямо из прогона Zig-примера: запросили 1 миллисекунду, получили 1290 микросекунд. На 29 % больше, и это нормальное поведение.</p>
  <p id="sHJ7">Для задач, где точность критична — мультимедиа, управление оборудованием, промышленные протоколы, — используют другие механизмы: таймеры высокого разрешения (<code>CLOCK_MONOTONIC</code> + <code>timerfd</code>), real-time планировщики, а в предельных случаях — busy-wait циклы, выжигающие ядро процессора ради нескольких микросекунд точности.</p>
  <h2 id="заблуждения-программистов-о-времени">Заблуждения программистов о времени</h2>
  <p id="Q8nZ">В 2012 году Ноа Суссман составил знаменитый список заблуждений — из тех, что все без исключения разделяют, пока не обожгутся. Вот самые цепкие:</p>
  <ul id="61DV">
    <li id="Lp5u"><strong>«В сутках 24 часа».</strong> Нет. При переходе на летнее время бывают 23- и 25-часовые сутки. При вставке високосной секунды — 86 401 секунда вместо 86 400;</li>
    <li id="yKhs"><strong>«В каждых сутках есть полночь».</strong> Нет. При весеннем переводе часов полночь может просто не существовать;</li>
    <li id="RCC4"><strong>«Часовых поясов — 24».</strong> Нет. Как мы уже говорили, их в районе 38, от UTC−12 до UTC+14;</li>
    <li id="Dj6o"><strong>«Смещения всегда кратны часу».</strong> Нет. UTC+5:30, UTC+5:45, UTC+12:45;</li>
    <li id="vrYK"><strong>«Разница между двумя часовыми поясами постоянна».</strong> Нет. Политики меняют смещения иногда за несколько дней до события;</li>
    <li id="XNY3"><strong>«Системные часы всегда идут вперёд».</strong> Нет. NTP, високосные секунды и ручная коррекция могут откатить время назад;</li>
    <li id="V6Sd"><strong>«Длительность системной минуты примерно равна настоящей».</strong> Нет. NTP-slewing может растягивать или сжимать частоту, чтобы мягко догонять атомное время.</li>
  </ul>
  <p id="KapN">Если вы удивились хотя бы одному пункту — продолжайте читать.</p>
  <h2 id="что-из-этого-следует">Что из этого следует</h2>
  <p id="5qFr">Время выглядит простым типом данных: число секунд от какой-то точки. На практике это одна из самых коварных абстракций в программировании. Несколько правил, которые помогают не попасть в историю:</p>
  <ol id="JIim">
    <li id="kVJ6"><strong>Используйте монотонные часы для измерения длительности.</strong> <code>Instant</code> в Rust, <code>time.Now()</code> + <code>time.Since()</code> в Go, <code>std.Io.Clock.Timestamp.now(io, .awake)</code> в Zig, <code>getMonoTime()</code> в Nim — все они защищают от прыжков wall clock. Если вы вычитаете два <code>SystemTime</code> или два <code>time.Parse()</code>-результата — вы уязвимы;</li>
    <li id="QBS1"><strong>Храните время в UTC.</strong> Часовой пояс — свойство отображения, а не хранения. Сохраняйте Unix timestamp или ISO 8601 с явным смещением, а не «локальное время» без контекста;</li>
    <li id="iDjc"><strong>Обновляйте tzdata.</strong> Часовые пояса — политическое решение, а не физическая константа. Ливан менял дату перехода на DST трижды за несколько недель в 2023 году. Если ваша база устарела, вы показываете неправильное время;</li>
    <li id="C3ZY"><strong>Не доверяйте <code>sleep()</code>.</strong> Он гарантирует минимум, а не точность. Для критичных интервалов используйте таймеры ОС;</li>
    <li id="bPOf"><strong>Готовьтесь к 2038 году.</strong> Если ваша система хранит время в <code>int32</code> — у вас есть 12 лет. Для многих встроенных систем этого не хватит.</li>
  </ol>
  <p id="Zh4c">Время — это не число. Это политика, астрономия, аппаратные причуды и десятки угловых случаев, каждый из которых кто-то уже превратил в продакшен-инцидент. Следующий инцидент, возможно, — ваш.</p>
  <h2 id="источники">Источники</h2>
  <ul id="zD5Z">
    <li id="rTmb"><a href="https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/" target="_blank">How and why the leap second affected Cloudflare DNS (Cloudflare, 2017)</a> — разбор того самого новогоднего падения</li>
    <li id="Ajoc"><a href="https://www.nature.com/articles/s41586-024-07170-0" target="_blank">A global timekeeping problem postponed by global warming (Nature, 2024)</a> — как таяние ледников отсрочило отрицательную секунду</li>
    <li id="g5VX"><a href="https://engineering.fb.com/2022/07/25/production-engineering/its-time-to-leave-the-leap-second-in-the-past/" target="_blank">It’s time to leave the leap second in the past (Meta, 2022)</a> — аргументы Meta против високосных секунд</li>
    <li id="zlDi"><a href="https://www.bipm.org/en/cgpm-2022/resolution-4" target="_blank">Resolution 4 of the 27th CGPM (2022)</a> — официальное решение об отмене високосных секунд к 2035 году</li>
    <li id="OOk6"><a href="https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time" target="_blank">Falsehoods programmers believe about time (Noah Sussman, 2012)</a> — классический список заблуждений</li>
    <li id="ABDg"><a href="https://go.googlesource.com/proposal/+/master/design/12914-monotonic.md" target="_blank">Go Proposal: Monotonic Elapsed Time Measurements</a> — как в Go появились монотонные часы</li>
    <li id="3AFN"><a href="https://doc.rust-lang.org/std/time/struct.Instant.html" target="_blank">Rust std::time::Instant</a> — документация монотонных часов Rust</li>
    <li id="MVBA"><a href="https://doc.rust-lang.org/std/time/struct.SystemTime.html" target="_blank">Rust std::time::SystemTime</a> — документация wall clock в Rust</li>
    <li id="YzQ2"><a href="https://ziglang.org/download/0.16.0/release-notes.html" target="_blank">Zig 0.16.0 Release Notes</a> — новая подсистема <code>std.Io</code></li>
    <li id="8Xc1"><a href="https://nim-lang.org/docs/times.html" target="_blank">Nim stdlib: times and monotimes</a> — документация модулей работы со временем в Nim</li>
    <li id="sRo3"><a href="https://en.wikipedia.org/wiki/Year_2038_problem" target="_blank">Year 2038 problem (Wikipedia)</a> — обзор проблемы Y2038</li>
    <li id="HIPT"><a href="https://www.gps.gov/news/gps-week-number-rollover" target="_blank">GPS Week Number Rollover (GPS.gov)</a> — переполнение счётчика GPS-недель</li>
    <li id="xihc"><a href="https://www.iana.org/time-zones" target="_blank">IANA Time Zone Database</a> — база часовых поясов</li>
    <li id="OMQG"><a href="https://en.wikipedia.org/wiki/Time_in_Samoa" target="_blank">Time in Samoa (Wikipedia)</a> — пропущенная пятница 30 декабря 2011 года</li>
    <li id="rMJ7"><a href="https://highearthorbit.com/articles/the-hidden-costs-of-daylight-saving-time/" target="_blank">The hidden costs of Daylight Saving Time (High Earth Orbit)</a> — стоимость DST для софта</li>
  </ul>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/zig-0.16.0</guid><link>https://azhark.cc/zig-0.16.0?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/zig-0.16.0?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Zig 0.16.0: Io как интерфейс, «сочный main» и новые билтины</title><pubDate>Thu, 16 Apr 2026 20:13:27 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/67/6e/676e4b34-a8eb-4348-8ae1-2c001ead53eb.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/51/20/5120059c-36a8-4117-905d-585c10e7246c.png"></img>Zig — язык, который пытается решить задачу, за которую мало кто берётся всерьёз: сделать системное программирование понятным и предсказуемым. Без скрытых аллокаций, без сборщика мусора, без магии в рантайме. Там, где C оставляет программиста наедине с неопределённым поведением, а C++ прячет сложность за слоями абстракций, Zig старается быть честным: если что-то происходит, это видно в коде.]]></description><content:encoded><![CDATA[
  <figure id="WSps" class="m_original">
    <img src="https://img2.teletype.in/files/51/20/5120059c-36a8-4117-905d-585c10e7246c.png" width="1536" />
  </figure>
  <p id="uR6i">Zig — язык, который пытается решить задачу, за которую мало кто берётся всерьёз: сделать системное программирование понятным и предсказуемым. Без скрытых аллокаций, без сборщика мусора, без магии в рантайме. Там, где C оставляет программиста наедине с неопределённым поведением, а C++ прячет сложность за слоями абстракций, Zig старается быть честным: если что-то происходит, это видно в коде.</p>
  <p id="xumT">Язык всё ещё не достиг версии 1.0, поэтому каждый релиз может переворачивать привычные вещи с ног на голову. Версия 0.16.0 — как раз такой случай. Это не косметическое обновление и не набор багфиксов. Здесь перестроено ядро стандартной библиотеки: весь ввод-вывод теперь проходит через единый интерфейс <code>Io</code>, который можно подменять, тестировать и отменять. Появился новый способ инициализации программы — «сочный main». <code>@Type</code> разобран на восемь специализированных билтинов (<em>builtins</em> — встроенные функции компилятора, которые начинаются с <code>@</code>). Компилятор стал строже к unsafe-паттернам, а в тулчейне обновился LLVM и появились новые платформы.</p>
  <p id="pPNN">Изменений много, и перечислять каждое — верный способ усыпить читателя. Поэтому ниже — только то, что действительно влияет на повседневную работу с языком: с примерами кода, сравнениями «было — стало» и объяснением, зачем всё это нужно.</p>
  <h2 id="сочный-main">«Сочный main»</h2>
  <p id="LFaB">Раньше типичный <code>main</code> в Zig начинался с ритуальной инициализации: достать аллокатор, разобрать аргументы, подготовить I/O. Теперь всё это приходит одним параметром:</p>
  <pre id="7pV9">const std = @import(&quot;std&quot;);

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;             // аллокатор общего назначения
    const io = init.io;               // экземпляр Io для всех операций ввода-вывода
    const arena = init.arena.allocator(); // арена для временных данных

    // аргументы командной строки — тоже через init
    const args = try init.minimal.args.toSlice(arena);

    std.log.info(&quot;первый аргумент: {s}&quot;, .{args[0]});
}
</pre>
  <p id="gqsx">Никакого шаблонного кода для запуска. Аллокатор, I/O и арена готовы к использованию — остаётся писать логику.</p>
  <h2 id="io-как-интерфейс-главное-архитектурное-изменение">Io как интерфейс: главное архитектурное изменение</h2>
  <p id="a692">Это центральная идея релиза: <strong>все</strong> операции ввода-вывода теперь принимают параметр <code>io</code>. Файлы, сеть, процессы, генерация случайных чисел, примитивы синхронизации — всё проходит через единый интерфейс <code>std.Io</code>.</p>
  <p id="F4ys">Зачем это нужно:</p>
  <ul id="ndDo">
    <li id="BPKF"><strong>Тестируемость.</strong> В тестах используется <code>std.testing.io</code> — не нужно подменять реальный I/O заглушками вручную.</li>
    <li id="wAvz"><strong>Сменяемость бэкенда.</strong> Один и тот же код работает поверх потоков, io_uring, kqueue или Grand Central Dispatch.</li>
    <li id="bRED"><strong>Отмена операций.</strong> Любую I/O-операцию можно отменить через единый механизм.</li>
  </ul>
  <h3 id="бэкенды-io">Бэкенды Io</h3>
  <figure id="8wsi" class="m_retina">
    <img src="https://img2.teletype.in/files/91/5a/915ac481-da9a-4dbe-8725-a3f2611e74a4.png" width="784" />
  </figure>
  <p id="XPBv">Для миграции с 0.15.x подходит <code>Io.Threaded</code> — он полнофункционален и хорошо протестирован.</p>
  <h3 id="простой-пример-http-запрос">Простой пример: HTTP-запрос</h3>
  <p id="xD82">Вот как выглядит HTTP HEAD-запрос с новым интерфейсом. DNS-резолвинг, TCP-соединение и отмена — всё работает через <code>io</code>:</p>
  <pre id="u7jr">const std = @import(&quot;std&quot;);
const Io = std.Io;

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;
    const args = try init.minimal.args.toSlice(init.arena.allocator());

    // DNS-имя парсится из аргумента командной строки
    const host_name: Io.net.HostName = try .init(args[1]);

    var http_client: std.http.Client = .{ .allocator = gpa, .io = io };
    defer http_client.deinit();

    var request = try http_client.request(.HEAD, .{
        .scheme = &quot;http&quot;,
        .host = .{ .percent_encoded = host_name.bytes },
        .port = 80,
        .path = .{ .percent_encoded = &quot;/&quot; },
    }, .{});
    defer request.deinit();

    try request.sendBodiless();

    var redirect_buffer: [1024]u8 = undefined;
    const response = try request.receiveHead(&amp;redirect_buffer);
    std.log.info(&quot;статус: {d} {s}&quot;, .{ response.head.status, response.head.reason });
}
</pre>
  <h3 id="future-и-group-конкурентность-без-колбэков">Future и Group: конкурентность без колбэков</h3>
  <p id="IrLw">Два ключевых инструмента для параллельного выполнения — <code>Future</code> и <code>Group</code>.</p>
  <p id="hdFb"><strong>Future</strong> — это отложенный результат одной операции. Запускаем функцию асинхронно, а результат получаем позже:</p>
  <pre id="Ol4M">// запускаем открытие файла асинхронно
var task = io.async(Io.Dir.openFile, .{ .cwd(), io, &quot;data.txt&quot;, .{} });

// при отмене или ошибке — корректно освобождаем ресурс
defer if (task.cancel(io)) |file| file.close(io) else |_| {};
</pre>
  <p id="NNM9"><strong>Group</strong> управляет множеством параллельных задач. Вот «сортировка сном» — каждый элемент засыпает на время, пропорциональное своему значению:</p>
  <pre id="g5AR">const std = @import(&quot;std&quot;);
const Io = std.Io;

test &quot;sleep sort&quot; {
    const io = std.testing.io; // тестовый Io — без настоящих системных вызовов

    const rng_impl: std.Random.IoSource = .{ .io = io };
    const rng = rng_impl.interface();

    var array: [10]i32 = undefined;
    for (&amp;array) |*elem| elem.* = rng.uintLessThan(u16, 1000);

    var sorted: [10]i32 = undefined;
    var index: std.atomic.Value(usize) = .init(0);

    var group: Io.Group = .init;
    defer group.cancel(io); // отмена всех незавершённых задач при выходе

    // запускаем все задачи параллельно
    for (&amp;array) |elem| group.async(io, sleepAppend, .{ io, &amp;sorted, &amp;index, elem });

    try group.await(io); // дожидаемся завершения всех

    // проверяем, что массив отсортирован
    for (sorted[0 .. sorted.len - 1], sorted[1..]) |a, b| {
        try std.testing.expect(a &lt;= b);
    }
}

fn sleepAppend(io: Io, result: []i32, i_ptr: *std.atomic.Value(usize), elem: i32) !void {
    // спим пропорционально значению элемента
    try io.sleep(.fromMilliseconds(elem), .awake);
    result[i_ptr.fetchAdd(1, .monotonic)] = elem;
}
</pre>
  <h3 id="отмена-операций">Отмена операций</h3>
  <p id="EioQ">Любая I/O-операция может быть отменена — через <code>error.Canceled</code> в наборе ошибок. Три стратегии работы с отменой:</p>
  <ol id="Hvqm">
    <li id="RKTP"><strong>Пробросить дальше</strong> — просто позволить ошибке всплыть по стеку.</li>
    <li id="U2Jy"><strong>Повторно установить отмену</strong> — вызвать <code>io.recancel()</code> после локальной обработки.</li>
    <li id="V23b"><strong>Защитить участок</strong> — временно отключить отмену через <code>io.swapCancelProtection()</code>.</li>
  </ol>
  <h2 id="новые-билтины-вместо-type">Новые билтины вместо @Type</h2>
  <p id="zwLG"><code>@Type</code> был универсальным, но громоздким. Теперь вместо одного обобщённого билтина — восемь специализированных: <code>@Int</code>, <code>@Tuple</code>, <code>@Pointer</code>, <code>@Fn</code>, <code>@Struct</code>, <code>@Union</code>, <code>@Enum</code> и <code>@EnumLiteral</code>.</p>
  <h3 id="int-и-tuple--самый-частый-случай">@Int и @Tuple — самый частый случай</h3>
  <pre id="lWka">// Было: 0.15.x
const U10 = @Type(.{ .int = .{ .signedness = .unsigned, .bits = 10 } });

// Стало: 0.16.0
const U10 = @Int(.unsigned, 10);
</pre>
  <pre id="CbNe">// Было: 0.15.x — 12 строк на объявление кортежа
const MyTuple = @Type(.{ .@&quot;struct&quot; = .{
    .layout = .auto,
    .fields = &amp;.{.{
        .name = &quot;0&quot;,
        .type = u32,
        .default_value_ptr = null,
        .is_comptime = false,
        .alignment = @alignOf(u32),
    }, .{
        .name = &quot;1&quot;,
        .type = [2]f64,
        .default_value_ptr = null,
        .is_comptime = false,
        .alignment = @alignOf([2]f64),
    }},
    .decls = &amp;.{},
    .is_tuple = true,
} });

// Стало: 0.16.0 — одна строка
const MyTuple = @Tuple(&amp;.{ u32, [2]f64 });
</pre>
  <h3 id="struct--генерация-структур-в-comptime">@Struct — генерация структур в comptime</h3>
  <p id="YeEM"><code>@Struct</code> позволяет создавать структуры с произвольными полями во время компиляции. Это полезно для метапрограммирования — например, генерации обёрток над перечислениями:</p>
  <pre id="NpVD">const std = @import(&quot;std&quot;);

// создать структуру, где каждому варианту перечисления соответствует поле
fn EnumFieldMap(comptime E: type, comptime FieldType: type) type {
    return @Struct(
        .auto,                          // layout
        null,                           // backing integer (нет)
        std.meta.fieldNames(E),         // имена полей — из перечисления
        &amp;@splat(FieldType),             // все поля одного типа
        &amp;@splat(.{}),                   // без дополнительных атрибутов
    );
}
</pre>
  <h3 id="enum--динамические-перечисления">@Enum — динамические перечисления</h3>
  <pre id="xING">// создаём enum с явным backing-типом и значениями
const Color = @Enum(
    u8,            // backing integer
    .exhaustive,   // исчерпывающий (не допускает неизвестных значений)
    &amp;.{ &quot;red&quot;, &quot;green&quot;, &quot;blue&quot; },
    &amp;.{ 0, 1, 2 },
);
</pre>
  <h3 id="что-не-стало-билтином">Что не стало билтином</h3>
  <p id="cy9S">Для <code>float</code>, <code>array</code>, <code>opaque</code>, <code>optional</code> и <code>error_union</code> отдельные билтины не нужны — используется обычный синтаксис языка. Наборы ошибок (<em>error sets</em>) тоже больше нельзя создавать через рефлексию: объявляйте их явно через <code>error { ... }</code>.</p>
  <h2 id="безопасность-на-уровне-языка">Безопасность на уровне языка</h2>
  <h3 id="запрет-на-возврат-адреса-локальной-переменной">Запрет на возврат адреса локальной переменной</h3>
  <p id="pCad">Классическая ошибка, которая в C приводит к <em>use-after-free</em>, теперь ловится на этапе компиляции:</p>
  <pre id="zoJ3">fn foo() *i32 {
    var x: i32 = 1234;
    return &amp;x;
    // ошибка компиляции: returning address of expired local variable &#x27;x&#x27;
}
</pre>
  <h3 id="указатели-в-packed-структурах-запрещены">Указатели в packed-структурах запрещены</h3>
  <p id="f75V">Указатели внутри <code>packed struct</code> и <code>packed union</code> больше не допускаются — вместо них нужно использовать <code>usize</code> с явным преобразованием через <code>@ptrFromInt</code>/<code>@intFromPtr</code>. Это исключает неопределённое поведение при работе с битовыми представлениями.</p>
  <h3 id="packed-union-все-поля-одного-размера">packed union: все поля одного размера</h3>
  <p id="w0Gi">Раньше можно было объявить <code>packed union</code> с полями разного размера — неиспользуемые биты просто игнорировались. Теперь это ошибка:</p>
  <pre id="7S8D">// Ошибка компиляции в 0.16.0: поля разного размера
const U = packed union {
    x: u8,
    y: u16,
};

// Правильно: явный backing-тип и выравнивание
const U = packed union(u16) {
    x: packed struct(u16) {
        data: u8,
        padding: u8 = 0,
    },
    y: u16,
};
</pre>
  <h3 id="явные-backing-типы-в-extern-контексте">Явные backing-типы в extern-контексте</h3>
  <p id="MiwY">Перечисления и packed-типы с неявным backing-типом больше нельзя использовать в <code>extern</code>-контексте. Компилятор требует указать тип явно:</p>
  <pre id="sHhq">// Ошибка в 0.16.0
const Status = enum { ok, err };
export var s: Status = .ok;

// Правильно
const Status = enum(u8) { ok, err };
export var s: Status = .ok;
</pre>
  <h3 id="запрет-на-runtime-индексацию-векторов">Запрет на runtime-индексацию векторов</h3>
  <p id="oUbr">SIMD-векторы больше нельзя индексировать переменной времени выполнения. Если нужен поэлементный доступ, приведите вектор к массиву:</p>
  <pre id="6vTI">// Ошибка в 0.16.0
for (0..vector_len) |i| {
    _ = vector[i]; // runtime index запрещён
}

// Правильно: приводим вектор к массиву
const info = @typeInfo(@TypeOf(vector)).vector;
const array: [info.len]info.child = vector;
for (&amp;array) |elem| {
    _ = elem;
}
</pre>
  <h2 id="конвертация-чисел-меньше-рутинного-кода">Конвертация чисел: меньше рутинного кода</h2>
  <h3 id="маленькие-целые-автоматически-приводятся-к-float">Маленькие целые автоматически приводятся к float</h3>
  <p id="kAnV">Целочисленные типы до 24 бит включительно теперь неявно приводятся к <code>f32</code> без потери точности, потому что мантисса <code>f32</code> вмещает все их значения:</p>
  <pre id="Xmqf">var foo_int: u24 = 123;
var foo_float: f32 = foo_int; // работает без @floatFromInt

var bar_int: u25 = 123;
var bar_float: f32 = @floatFromInt(bar_int); // u25 не помещается — нужна явная конвертация
</pre>
  <h3 id="floor-ceil-round-trunc-конвертируют-в-целые">@floor, @ceil, @round, @trunc конвертируют в целые</h3>
  <p id="GHVC">Раньше для округления <code>f32</code> до <code>u8</code> требовалась цепочка <code>@intFromFloat(@round(value))</code>. Теперь <code>@round</code> (и другие) умеют конвертировать напрямую:</p>
  <pre id="8UIG">const std = @import(&quot;std&quot;);

test &quot;round to int&quot; {
    try example(12, 12.34);
    try example(13, 12.50);
}

fn example(expected: u8, value: f32) !void {
    const actual: u8 = @round(value); // float -&gt; int за один шаг
    try std.testing.expectEqual(expected, actual);
}
</pre>
  <p id="MEKT"><code>@intFromFloat</code> для этих случаев помечен как <em>deprecated</em>.</p>
  <h2 id="стандартная-библиотека-что-ещё-нового">Стандартная библиотека: что ещё нового</h2>
  <h3 id="deflate-сжатие-на-чистом-zig">Deflate-сжатие на чистом Zig</h3>
  <p id="zaxx">В стандартной библиотеке появилась реализация deflate-компрессора. Раньше была только декомпрессия. Три режима работы:</p>
  <ul id="XVID">
    <li id="fYG7"><strong>Standard</strong> — полноценное сжатие (по умолчанию);</li>
    <li id="LMdu"><strong>Raw</strong> — хранение без сжатия;</li>
    <li id="tFav"><strong>Huffman</strong> — сжатие кодами Хаффмана без поиска совпадений.</li>
  </ul>
  <p id="B5K3">Производительность сопоставима с zlib:</p>
  <figure id="LAiw" class="m_retina">
    <img src="https://img3.teletype.in/files/2c/be/2cbe9bce-11b5-44f0-bf04-7f2b6563b8f3.png" width="787" />
  </figure>
  <p id="btTm">Для большинства задач разница в степени сжатия незаметна, а выигрыш в скорости ощутим. И никакой зависимости от C-библиотеки.</p>
  <h3 id="arenaallocator-стал-потокобезопасным">ArenaAllocator стал потокобезопасным</h3>
  <p id="XXA3"><code>std.heap.ArenaAllocator</code> теперь использует атомарные операции вместо мьютекса. В однопоточном режиме скорость не изменилась, а при использовании из нескольких потоков (до ~7) — заметно быстрее, чем старый вариант с <code>ThreadSafeAllocator</code>. Сам <code>ThreadSafeAllocator</code> удалён за ненадобностью.</p>
  <h3 id="новые-криптоалгоритмы">Новые криптоалгоритмы</h3>
  <p id="kDl9">Добавлены реализации AES-SIV, AES-GCM-SIV, а также семейство Ascon (Ascon-AEAD, Ascon-Hash, Ascon-CHash). Всё на чистом Zig, без внешних зависимостей.</p>
  <h2 id="cimport-уходит-в-build-систему">@cImport уходит в build-систему</h2>
  <p id="tnH0"><code>@cImport</code> помечен как <em>deprecated</em>. Трансляция C-заголовков теперь выполняется через <code>addTranslateC</code> в <code>build.zig</code>:</p>
  <pre id="Uksn">// build.zig
const translate_c = b.addTranslateC(.{
    .root_source_file = b.path(&quot;src/c.h&quot;),
    .target = target,
    .optimize = optimize,
});
translate_c.linkSystemLibrary(&quot;glfw&quot;, .{});
translate_c.linkSystemLibrary(&quot;epoxy&quot;, .{});

const exe = b.addExecutable(.{
    .name = &quot;app&quot;,
    .root_module = b.createModule(.{
        .root_source_file = b.path(&quot;src/main.zig&quot;),
        .optimize = optimize,
        .target = target,
        .imports = &amp;.{
            .{ .name = &quot;c&quot;, .module = translate_c.createModule() },
        },
    }),
});
</pre>
  <p id="vOFI">А в коде вместо <code>@cImport</code> — обычный импорт:</p>
  <pre id="Cqpw">const c = @import(&quot;c&quot;);
</pre>
  <p id="Wu5V">Сами заголовки живут в обычном <code>.h</code>-файле:</p>
  <pre id="6lFn">// src/c.h
#include &lt;stdio.h&gt;
#include &lt;GLFW/glfw3.h&gt;
#include &lt;epoxy/gl.h&gt;
</pre>
  <p id="FhgC">Преимущества: C-трансляция встроена в граф зависимостей сборки, работает кэширование, а <code>build.zig</code> полностью описывает все внешние зависимости.</p>
  <h2 id="компилятор-и-тулчейн">Компилятор и тулчейн</h2>
  <h3 id="llvm-21">LLVM 21</h3>
  <p id="ueIz">Компилятор обновлён до LLVM 21. Из-за регрессии в LLVM временно отключена векторизация циклов. Её вернут, когда апстрим исправит баг.</p>
  <h3 id="новые-платформы-второго-уровня-тестируются-в-ci">Новые платформы второго уровня (тестируются в CI)</h3>
  <p id="XNdB">К уже поддерживаемым платформам добавлены:</p>
  <ul id="1GOi">
    <li id="rE4m"><code>aarch64-freebsd</code>, <code>aarch64-netbsd</code>;</li>
    <li id="dXFW"><code>x86_64-freebsd</code>, <code>x86_64-netbsd</code>, <code>x86_64-openbsd</code>;</li>
    <li id="ciq0"><code>loongarch64-linux</code>, <code>powerpc64le-linux</code>, <code>s390x-linux</code>.</li>
  </ul>
  <p id="rTfc">Появилась базовая поддержка экзотических архитектур: Alpha, KVX, MicroBlaze, OpenRISC, PA-RISC, SuperH. Убрана поддержка Oracle Solaris, IBM AIX и IBM z/OS (illumos остаётся).</p>
  <h3 id="фаззер-smith">Фаззер Smith</h3>
  <p id="w8Cf">Новый AST-фаззер Smith генерирует синтаксически корректные программы на Zig и проверяет, что компилятор не падает. Поддерживается мультипроцессный фаззинг, бесконечный режим и автоматическое сохранение краш-дампов. С помощью Smith уже найдены и исправлены десятки багов.</p>
  <h3 id="пакетный-менеджер">Пакетный менеджер</h3>
  <p id="65nN">Два полезных улучшения:</p>
  <ul id="nWS2">
    <li id="KlzK"><strong>Локальные переопределения пакетов</strong> — можно указать локальный путь вместо удалённой зависимости, удобно для разработки форков.</li>
    <li id="oFtV"><strong>Загрузка в каталог проекта</strong> — пакеты можно скачивать не в глобальный кэш, а в директорию проекта.</li>
  </ul>
  <h2 id="миграция-с-015x-всё-в-одном-месте">Миграция с 0.15.x: всё в одном месте</h2>
  <p id="SlkB">Основной паттерн миграции: добавить параметр <code>io</code> к вызовам I/O-функций. Получить <code>io</code> проще всего через «сочный main» (<code>std.process.Init</code>), а в тестах — через <code>std.testing.io</code>.</p>
  <h3 id="файловая-система">Файловая система</h3>
  <figure id="CqLt" class="m_retina">
    <img src="https://img2.teletype.in/files/11/dd/11dd500a-c2e1-40af-8abc-25c9d24aa9cb.png" width="781" />
  </figure>
  <h3 id="примитивы-синхронизации">Примитивы синхронизации</h3>
  <figure id="wrzg" class="m_retina">
    <img src="https://img4.teletype.in/files/30/d3/30d3525a-11b4-4d60-b426-3a3a45549f76.png" width="788" />
  </figure>
  <h3 id="процессы">Процессы</h3>
  <figure id="zJQB" class="m_retina">
    <img src="https://img2.teletype.in/files/57/9f/579feb67-e8b0-4ff8-8264-660f3abbc42f.png" width="789" />
  </figure>
  <h3 id="случайные-числа">Случайные числа</h3>
  <figure id="sNxV" class="m_retina">
    <img src="https://img1.teletype.in/files/41/fc/41fc40da-1d8c-4041-8c14-496f93503f44.png" width="782" />
  </figure>
  <h3 id="время">Время</h3>
  <figure id="ziki" class="m_retina">
    <img src="https://img1.teletype.in/files/8d/77/8d77a2e6-070f-436f-8f0d-d89c844dd893.png" width="777" />
  </figure>
  <h3 id="коды-ошибок">Коды ошибок</h3>
  <figure id="H8Qr" class="m_retina">
    <img src="https://img2.teletype.in/files/d1/cd/d1cdcf50-9d38-48b3-a561-2642727122ad.png" width="786" />
  </figure>
  <h3 id="удалено-без-замены">Удалено без замены</h3>
  <ul id="VPoT">
    <li id="JPp6"><code>std.heap.ThreadSafeAllocator</code> — <code>ArenaAllocator</code> теперь потокобезопасен сам по себе;</li>
    <li id="Q4dX"><code>std.Thread.Mutex.Recursive</code> — перепроектируйте код без рекурсивных блокировок;</li>
    <li id="w06u"><code>std.once</code> — избегайте ленивой инициализации глобального состояния;</li>
    <li id="ZGqT"><code>std.SegmentedList</code> — используйте <code>ArrayList</code> или другие контейнеры;</li>
    <li id="Rq03"><code>std.Io.GenericReader</code>, <code>std.Io.AnyReader</code>, <code>std.Io.FixedBufferStream</code> — заменены новым I/O-интерфейсом;</li>
    <li id="mLSX"><code>std.fs.getAppDataDir</code> — без замены;</li>
    <li id="cSoc">Z/W-суффиксные варианты функций файловой системы (<code>realpathZ</code>, <code>openDirAbsoluteZ</code>, <code>deleteDirZ</code> и т. д.) — используйте основные функции из <code>std.Io.Dir</code>.</li>
  </ul>
  <h2 id="итого">Итого</h2>
  <p id="Y5uR">Zig 0.16.0 — это прежде всего архитектурный сдвиг. Io-интерфейс пронизывает всю стандартную библиотеку, делая код тестируемым и переносимым между бэкендами. Новые билтины упрощают метапрограммирование, а усиленные проверки компилятора ловят ошибки, которые раньше приводили к UB в рантайме.</p>
  <p id="oQ46">Миграция с 0.15.x потребует усилий — API файловой системы, потоков и процессов переехали почти целиком. Но паттерн изменений единообразный: добавить <code>io</code> в параметры и обновить пути импортов. Полный список изменений — в <a href="https://ziglang.org/download/0.16.0/release-notes.html" target="_blank">официальных release notes</a>.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/dieta</guid><link>https://azhark.cc/dieta?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/dieta?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Диета при жировом гепатозе и хроническом панкреатите</title><pubDate>Wed, 15 Apr 2026 19:29:24 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/74/20/7420871b-eb57-44bc-8701-9eda79486f62.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/1a/94/1a94b199-0821-44a2-8e11-c4344083cc6f.png"></img>Сходил сегодня на УЗИ, и меня «обрадовали» жировым гепатозом и — неожиданно — хроническим панкреатитом. И если против гепатоза никаких возражений нет — вполне закономерно при ИМТ 37, то вот насчёт панкреатита, да ещё и хронического, у меня большие сомнения.]]></description><content:encoded><![CDATA[
  <figure id="ttf8" class="m_original">
    <img src="https://img2.teletype.in/files/1a/94/1a94b199-0821-44a2-8e11-c4344083cc6f.png" width="1536" />
  </figure>
  <p id="tOqm">Сходил сегодня на УЗИ, и меня «обрадовали» жировым гепатозом и — неожиданно — хроническим панкреатитом. И если против гепатоза никаких возражений нет — вполне закономерно при ИМТ 37, то вот насчёт панкреатита, да ещё и хронического, у меня большие сомнения.</p>
  <p id="zoPQ">Тем не менее, врач сказала, что поджелудочная не в порядке. Значит, стоит обратиться к врачу и принять меры.</p>
  <p id="cng4">Ещё у меня не в порядке почки. Об этом я тоже знал, УЗИ просто подтвердило, что мне нужно обратиться к урологу.</p>
  <p id="WfL4">Первое дело при гепатозе и панкреатите — правильная диета и снижение веса до нормы. Так что изучил клинические рекомендации и составил примерное меню, список покупок и рецепты. Ну и принципы питания, куда же без них.</p>
  <p id="gWpD">Чтобы не потерять проделанную работу в бесконечном количестве документов и текстов, решил опубликовать это в блоге. Тем более, что это может пригодиться не только мне.</p>
  <hr />
  <p id="Aefi">При жировом гепатозе и хроническом панкреатите вне обострения подходит одна и та же базовая схема питания. Ниже — принципы, меню на неделю, список покупок и рецепты.</p>
  <blockquote id="He73"><strong>Важно.</strong> Это меню подходит как базовый вариант <strong>вне острого приступа</strong>. При сильной боли, рвоте, температуре или резком ухудшении после еды необходима очная медицинская помощь, а не домашняя диета.</blockquote>
  <h2 id="принципы-питания">Принципы питания</h2>
  <h3 id="общие-для-обоих-состояний">Общие для обоих состояний</h3>
  <ul id="NGeH">
    <li id="FWOm">4–5 приёмов пищи в день умеренными порциями.</li>
    <li id="WEBW">Способы готовки: варка, запекание, тушение, на пару.</li>
    <li id="mBoj">Пить воду регулярно в течение дня.</li>
    <li id="umlq">Если конкретный продукт вызывает боль, тяжесть, тошноту или жидкий стул — временно исключить и обсудить с врачом.</li>
  </ul>
  <h3 id="при-жировом-гепатозе">При жировом гепатозе</h3>
  <ul id="L8ls">
    <li id="LL5q">Главная цель — постепенное снижение веса, если есть избыток. Снижение массы тела на 7–10 % даёт заметное улучшение состояния печени.</li>
    <li id="glkE">Полезны овощи, цельные злаки, жирная рыба (источник омега-3), оливковое масло, орехи в умеренном количестве — всё это элементы средиземноморской диеты.</li>
    <li id="DxV7">Для печени особенно вредна фруктоза в больших количествах — ограничивать не только сахар, но и фруктовые соки.</li>
    <li id="2Ou8">Кофе без сахара допустим и даже полезен: есть данные о его защитном действии на печень. И это меня очень-очень радует, как большого любителя этого прекрасного напитка.</li>
  </ul>
  <h3 id="при-хроническом-панкреатите-вне-обострения">При хроническом панкреатите вне обострения</h3>
  <ul id="RN3A">
    <li id="Fe8n">Пожизненная сверхжёсткая обезжиренная диета не нужна всем подряд — жирность рациона подбирается по симптомам.</li>
    <li id="n0a1">При недостаточности ферментов врач может назначить ферментные препараты. Именно они позволяют не урезать жиры до минимума, а питаться нормально.</li>
    <li id="Qur8">Бобовые полезны по составу, но могут вызывать вздутие — вводить постепенно, по переносимости.</li>
    <li id="B3L8">Слишком много клетчатки за раз может снижать эффективность ферментных препаратов. Умеренное количество овощей и круп — нормально; увлекаться отрубями и сырыми салатами в больших объёмах не стоит.</li>
    <li id="NnQh">Важно контролировать уровень жирорастворимых витаминов (A, D, E, K) и при необходимости принимать их дополнительно — обсудить с врачом.</li>
  </ul>
  <h2 id="что-есть">Что есть</h2>
  <figure id="WNK1" class="m_retina">
    <img src="https://img3.teletype.in/files/61/e5/61e50e15-0969-434d-8797-e21ce762e688.png" width="784" />
  </figure>
  <p id="2GXf">Продукты внутри каждой категории взаимозаменяемы: например, курицу можно заменить индейкой или рыбой, гречку — рисом или картофелем, кабачки — брокколи или цветной капустой.</p>
  <h2 id="что-исключить">Что исключить</h2>
  <ul id="yZMx">
    <li id="SMq5">алкоголь;</li>
    <li id="ArJI">жареное;</li>
    <li id="ODxv">жирное мясо (сало, жирная свинина);</li>
    <li id="8Wgg">колбасы, сосиски, копчёности;</li>
    <li id="p5cM">фастфуд (бургеры, пицца);</li>
    <li id="PdZn">сладкая газировка, соки в большом количестве;</li>
    <li id="Q5kw">торты, сладкая выпечка как повседневная еда;</li>
    <li id="DmjL">майонез, жирные соусы.</li>
  </ul>
  <h2 id="ориентир-по-порциям">Ориентир по порциям</h2>
  <figure id="RXK8" class="m_retina">
    <img src="https://img4.teletype.in/files/b2/ca/b2ca599b-7365-4b76-b8a8-97866285e9d9.png" width="786" />
  </figure>
  <h2 id="меню-на-неделю">Меню на неделю</h2>
  <figure id="n3Az" class="m_retina">
    <img src="https://img4.teletype.in/files/f7/ec/f7ec11db-77c8-45d6-a3b3-0e42c06156f0.png" width="784" />
  </figure>
  <p id="lBsk">При сильном голоде можно добавить ещё один небольшой перекус: кефир, 100–150 г творога или печёное яблоко.</p>
  <h2 id="список-покупок-на-неделю">Список покупок на неделю</h2>
  <h3 id="белок">Белок</h3>
  <ul id="Bq1p">
    <li id="XXYS">Куриная грудка — 1,2–1,5 кг.</li>
    <li id="hmHm">Индейка (филе или фарш) — 800 г – 1 кг.</li>
    <li id="k80x">Нежирная рыба (хек, минтай, треска) — 800 г – 1 кг.</li>
    <li id="j3Qw">Лосось или скумбрия — 300–400 г (на 1–2 приёма).</li>
    <li id="Z4PD">Яйца — 15 шт.</li>
    <li id="ClJL">Творог 2–5 % — 1,2–1,5 кг.</li>
    <li id="1JQu">Йогурт без сахара — 7 шт. (~150 г каждый) или 1 л.</li>
    <li id="W8pU">Кефир 1 % — 1–1,5 л.</li>
  </ul>
  <h3 id="крупы-и-гарниры">Крупы и гарниры</h3>
  <ul id="5J13">
    <li id="xHm8">Овсяные хлопья — 300–400 г.</li>
    <li id="myqs">Гречка — 400–500 г.</li>
    <li id="HQoc">Рис — 400–500 г.</li>
    <li id="cOV8">Вермишель — 1 пачка (400 г).</li>
    <li id="M3W3">Картофель — 1,5–2 кг.</li>
    <li id="lW5t">Хлебцы или галеты — 1 пачка.</li>
  </ul>
  <h3 id="овощи">Овощи</h3>
  <ul id="sgyT">
    <li id="Y6ag">Кабачки — 1–1,5 кг.</li>
    <li id="JBns">Морковь — 700 г – 1 кг.</li>
    <li id="wnhE">Цветная капуста — 1 кочан (~500 г) или замороженная.</li>
    <li id="9qYM">Брокколи — 500–700 г.</li>
    <li id="vpqF">Тыква — 500 г – 1 кг (по желанию).</li>
    <li id="D4Dh">Свёкла — 500 г (по переносимости).</li>
  </ul>
  <h3 id="фрукты">Фрукты</h3>
  <ul id="LzJR">
    <li id="wUGn">Яблоки — 1 кг.</li>
    <li id="9Kad">Бананы — 5–7 шт.</li>
    <li id="uTib">Груши мягкие — 3–4 шт.</li>
  </ul>
  <h3 id="дополнительно">Дополнительно</h3>
  <ul id="fgo0">
    <li id="TQLw">Молоко 1–1,5 % — 1 л.</li>
    <li id="FWW0">Оливковое масло — 1 бутылка (250–500 мл).</li>
    <li id="ZfHR">Чай, кофе.</li>
    <li id="kgqr">Соль.</li>
  </ul>
  <hr />
  <h2 id="рецепты">Рецепты</h2>
  <h3 id="1-овсянка">1. Овсянка</h3>
  <p id="Rkru"><strong>Ингредиенты:</strong> 50 г овсяных хлопьев, 200 мл воды (или 100 мл воды + 100 мл молока 1,5 %), щепотка соли.</p>
  <p id="RbOJ"><strong>Приготовление.</strong> Залить жидкостью, варить 5–7 минут, помешивая. По желанию добавить половину банана или 50 г тёртого яблока.</p>
  <p id="pl54"><strong>На выходе:</strong> ~250 г.</p>
  <hr />
  <h3 id="2-омлет-на-пару">2. Омлет на пару</h3>
  <p id="xhzS"><strong>Ингредиенты:</strong> 2 яйца, 40 мл молока 1,5 %, щепотка соли.</p>
  <p id="r0uR"><strong>Приготовление.</strong> Взбить вилкой, вылить в форму. Готовить на пару, в мультиварке, в микроволновке (2–3 минуты) или на сковороде под крышкой на минимальном огне без масла.</p>
  <hr />
  <h3 id="3-куриная-грудка-в-духовке">3. Куриная грудка в духовке</h3>
  <p id="jExD"><strong>Ингредиенты:</strong> 700–800 г куриной грудки, ½ ч. л. соли, 50 мл воды.</p>
  <p id="2VfX"><strong>Приготовление.</strong> Нарезать грудку на куски толщиной 2–3 см, посолить, выложить в форму, добавить воду, плотно накрыть фольгой. Запекать при 180 °C 25–30 минут.</p>
  <p id="LBip"><strong>Хватает на:</strong> 2–3 дня. Использовать с любым гарниром и овощами.</p>
  <hr />
  <h3 id="4-тефтели-из-индейки">4. Тефтели из индейки</h3>
  <p id="m08V"><strong>Ингредиенты:</strong> 500 г фарша индейки, 1 яйцо, 50 г отварного риса (по желанию), ½ ч. л. соли.</p>
  <p id="m0rS"><strong>Приготовление.</strong> Смешать все ингредиенты, сформировать шарики диаметром ~4 см (~15–18 штук), выложить в форму, добавить 50 мл воды, накрыть фольгой. Запекать при 180 °C 25–30 минут.</p>
  <p id="qEve"><strong>Порция:</strong> 4–5 штук.</p>
  <hr />
  <h3 id="5-рыба-запечённая">5. Рыба запечённая</h3>
  <p id="eVfH"><strong>Ингредиенты:</strong> 400–500 г филе (хек, минтай или треска), ¼ ч. л. соли, 30 мл воды.</p>
  <p id="Y9YN"><strong>Приготовление.</strong> Выложить филе в форму, посолить, добавить воду, накрыть фольгой. Запекать при 180 °C 20–25 минут. Подавать с картофелем, рисом или овощным пюре.</p>
  <hr />
  <h3 id="6-суп-с-курицей">6. Суп с курицей</h3>
  <p id="ejHE"><strong>Ингредиенты:</strong> 300 г куриной грудки, 300 г картофеля (2–3 шт.), 100 г моркови (1 средняя), 50 г вермишели или 40 г риса, 1,5 л воды, ½ ч. л. соли.</p>
  <p id="8zIh"><strong>Приготовление.</strong> Курицу залить водой, довести до кипения, снять пену. Добавить нарезанный картофель и морковь. Варить 15 минут. Добавить вермишель или рис, варить ещё 7–10 минут. Без зажарки.</p>
  <p id="9GMC"><strong>Хватает на:</strong> 2–3 дня.</p>
  <hr />
  <h3 id="7-овощной-суп-пюре">7. Овощной суп-пюре</h3>
  <p id="QVVk"><strong>Ингредиенты:</strong> 200 г кабачка, 150 г моркови (1 крупная), 200 г цветной капусты или брокколи, 200 г картофеля (1–2 шт.), 1 л воды, ½ ч. л. соли.</p>
  <p id="koKY"><strong>Приготовление.</strong> Всё нарезать, залить водой, варить до мягкости (~20 минут). Измельчить блендером. Если слишком густо — добавить воды от варки.</p>
  <p id="MIHr"><strong>Хватает на:</strong> 2 дня.</p>
  <hr />
  <h3 id="8-рыбный-суп">8. Рыбный суп</h3>
  <p id="Gy9z"><strong>Ингредиенты:</strong> 300 г филе белой рыбы, 300 г картофеля (2–3 шт.), 100 г моркови (1 шт.), 1,2 л воды, ½ ч. л. соли.</p>
  <p id="xWWI"><strong>Приготовление.</strong> Картофель и морковь нарезать, залить водой, варить 15 минут. Добавить рыбу кусками, варить ещё 10 минут. Без зажарки.</p>
  <hr />
  <h3 id="9-картофельное-пюре">9. Картофельное пюре</h3>
  <p id="Xw74"><strong>Ингредиенты:</strong> 500 г картофеля (4–5 средних клубней), 50–70 мл тёплого молока 1,5 %, щепотка соли. Масло — не более 5 г или без него.</p>
  <p id="Gwg7"><strong>Приготовление.</strong> Картофель очистить, отварить до мягкости (~20 минут), слить воду. Размять, добавить тёплое молоко, перемешать до однородности.</p>
  <hr />
  <h3 id="10-творожная-запеканка">10. Творожная запеканка</h3>
  <p id="Nz48"><strong>Ингредиенты:</strong> 400 г творога 2–5 %, 1 яйцо, 40 г овсяных хлопьев или манки. Без сахара или с минимумом подсластителя.</p>
  <p id="uQ2D"><strong>Приготовление.</strong> Смешать все ингредиенты, выложить в форму (можно застелить пергаментом). Запекать при 180 °C 30–35 минут до золотистой корочки.</p>
  <p id="v1cO"><strong>На выходе:</strong> ~500 г. Порция — 200–250 г.</p>
  <hr />
  <h3 id="11-тушёные-кабачки-с-морковью">11. Тушёные кабачки с морковью</h3>
  <p id="4eg0"><strong>Ингредиенты:</strong> 400 г кабачков, 100 г моркови (1 шт.), 50 мл воды, щепотка соли.</p>
  <p id="sztn"><strong>Приготовление.</strong> Кабачки нарезать кубиками, морковь — мелко или на тёрке. Сложить в кастрюлю, добавить воду, тушить под крышкой 15–20 минут до мягкости.</p>
  <hr />
  <h3 id="12-брокколи-или-цветная-капуста-на-пару">12. Брокколи или цветная капуста на пару</h3>
  <p id="PF7P"><strong>Ингредиенты:</strong> 400 г соцветий, щепотка соли.</p>
  <p id="5SWi"><strong>Приготовление.</strong> Разобрать на соцветия, готовить на пару или отварить в небольшом количестве воды 10–12 минут до мягкости.</p>
  <hr />
  <h2 id="план-готовки-на-неделю">План готовки на неделю</h2>
  <p id="Saqz">Чтобы не стоять у плиты каждый день, достаточно двух сессий готовки.</p>
  <h3 id="воскресенье--база-на-понедельниксреду">Воскресенье — база на понедельник–среду</h3>
  <p id="zOjk"><strong>Белок:</strong></p>
  <ul id="ceuD">
    <li id="ue1R">запечь 700–800 г куриной грудки (рецепт 3);</li>
    <li id="9c4B">сделать тефтели из 500 г фарша индейки (рецепт 4).</li>
  </ul>
  <p id="6Yoj"><strong>Гарниры:</strong></p>
  <ul id="QZe0">
    <li id="z1Qg">сварить кастрюлю гречки (~200 г сухой);</li>
    <li id="cYfh">сварить кастрюлю риса (~200 г сухого);</li>
    <li id="K1Wv">отварить картофель (~500 г).</li>
  </ul>
  <p id="EBdB"><strong>Овощи:</strong></p>
  <ul id="wOSu">
    <li id="9CKu">потушить кабачки с морковью (рецепт 11);</li>
    <li id="R4Cj">отварить или приготовить на пару брокколи / цветную капусту (рецепт 12).</li>
  </ul>
  <p id="UGD5"><strong>Суп:</strong></p>
  <ul id="ubrL">
    <li id="OIEF">сварить суп с курицей на 2–3 дня (рецепт 6).</li>
  </ul>
  <p id="BAH1"><strong>Завтраки:</strong></p>
  <ul id="Zhvt">
    <li id="usRq">сделать творожную запеканку (рецепт 10);</li>
    <li id="g5Ja">держать наготове творог, яйца, овсянку.</li>
  </ul>
  <h3 id="среда--база-на-четвергвоскресенье">Среда — база на четверг–воскресенье</h3>
  <p id="fgwF"><strong>Белок:</strong></p>
  <ul id="7LAK">
    <li id="1xzJ">запечь 400–500 г рыбы (рецепт 5);</li>
    <li id="vOfG">запечь 500–700 г курицы или индейки.</li>
  </ul>
  <p id="jXxJ"><strong>Гарниры:</strong></p>
  <ul id="Ysp5">
    <li id="ZkIi">сварить гречку или рис;</li>
    <li id="Ai04">приготовить картофельное пюре (рецепт 9).</li>
  </ul>
  <p id="u3S2"><strong>Овощи:</strong></p>
  <ul id="zVdU">
    <li id="VBSV">потушить новую порцию овощей;</li>
    <li id="LljG">сварить овощной суп-пюре (рецепт 7) или рыбный суп (рецепт 8).</li>
  </ul>
  <p id="SYsD"><strong>Перекусы — докупить/пополнить:</strong></p>
  <ul id="Ozy7">
    <li id="qJOb">творог, йогурт, кефир, бананы, яблоки.</li>
  </ul>
  <h2 id="быстрая-сборка-из-заготовок">Быстрая сборка из заготовок</h2>
  <p id="GK8I">Из готовой базы еда собирается за несколько минут:</p>
  <ol id="Yahp">
    <li id="y8J8">Курица + гречка + тушёные кабачки.</li>
    <li id="hzgD">Тефтели из индейки + рис + брокколи.</li>
    <li id="giVV">Рыба + картофельное пюре + цветная капуста.</li>
    <li id="mj3i">Суп + хлебцы; творог на перекус.</li>
    <li id="TsDp">Творожная запеканка + йогурт.</li>
  </ol>
  <h2 id="памятка">Памятка</h2>
  <p id="gHZ1">Не жарить. Не пить алкоголь. Есть 4–5 раз в день. Белок — в каждый основной приём пищи. Гарнир — умеренно. Сладкое и фастфуд — убрать. При избыточном весе — стремиться к постепенному снижению на 7–10 % от текущей массы тела.</p>
  <hr />
  <h3 id="источники">Источники</h3>
  <ul id="T9BC">
    <li id="bdGA"><a href="https://www.niddk.nih.gov/health-information/liver-disease/nafld-nash/eating-diet-nutrition" target="_blank">NIDDK — Eating, Diet, &amp; Nutrition for NAFLD &amp; NASH</a></li>
    <li id="6itq"><a href="https://www.niddk.nih.gov/health-information/digestive-diseases/pancreatitis/eating-diet-nutrition" target="_blank">NIDDK — Eating, Diet, &amp; Nutrition for Pancreatitis</a></li>
    <li id="l05G"><a href="https://www.niddk.nih.gov/health-information/liver-disease/nafld-nash/treatment" target="_blank">NIDDK — Treatment for NAFLD &amp; NASH</a></li>
    <li id="vfd9"><a href="https://www.journal-of-hepatology.eu/article/S0168-8278(24)00329-5/fulltext" target="_blank">EASL–EASD–EASO Clinical Practice Guidelines on MASLD (2024)</a></li>
    <li id="qXTK"><a href="https://www.clinicalnutritionjournal.com/article/S0261-5614(23)00459-4/fulltext" target="_blank">ESPEN Practical Guideline on Nutrition in Chronic Pancreatitis (2023)</a></li>
  </ul>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/collector-of-silence</guid><link>https://azhark.cc/collector-of-silence?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/collector-of-silence?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Коллекционер тишины</title><pubDate>Wed, 15 Apr 2026 11:38:31 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/87/25/87259a23-4218-4e8d-8b60-46f729dab603.png"></media:content><description><![CDATA[<img src="https://img4.teletype.in/files/f5/c5/f5c5a16f-b093-4aa0-b8a0-1bab7785a803.png"></img>Семён Кротов собирал тишину.]]></description><content:encoded><![CDATA[
  <figure id="Kz4p" class="m_original">
    <img src="https://img4.teletype.in/files/f5/c5/f5c5a16f-b093-4aa0-b8a0-1bab7785a803.png" width="1536" />
  </figure>
  <figure id="AksQ" class="m_column">
    <iframe src="https://music.yandex.ru/iframe/#track/150205596/41156923"></iframe>
    <figcaption>Слушать рассказ на Яндекс Музыке</figcaption>
  </figure>
  <p id="sHFi">Семён Кротов собирал тишину.</p>
  <p id="rCNz">Не звуки — именно тишину. У него была целая коллекция: тишина заброшенной церкви в трёхчасовой банке, тишина первого снега в маленькой шкатулке, тишина между ударами сердца умирающего в старом термосе, который он никогда не открывал.</p>
  <p id="tMiQ">Профессия звукорежиссёра научила Семёна различать оттенки безмолвия. Обычные люди думают, что тишина — это просто отсутствие шума. Какая наивность! Тишина живая, многослойная. Она имеет текстуру, вкус, даже запах.</p>
  <p id="9yyQ">Тишина детской игрушки, потерянной на пустыре — острая, пронзительная. Тишина старых книг в закрывшейся библиотеке — тёплая, чуть пыльная. А тишина между словами «я тебя больше не люблю» — она горькая, как полынь, и режет хуже ножа.</p>
  <p id="Lvnm">Семён ловил эту тишину специальным оборудованием собственной конструкции. Микрофоны, настроенные на улавливание пустоты, вакуумные контейнеры для хранения безмолвия. Коллеги считали его чудаком, но платили хорошо — его записи создавали невероятную атмосферу в фильмах.</p>
  <p id="X2co">Всё изменилось, когда он услышал объявление в метро:</p>
  <p id="1RGM">«Внимание! На станции «Тихая» возможна задержка поездов в связи с техническими работами».</p>
  <p id="iNa9">Проблема была в том, что станции «Тихая» в московском метро не существовало.</p>
  <p id="KWxr">Семён перепроверил схему — между «Сухаревской» и «Проспектом Мира» была только «Цветной бульвар». Спросил у других пассажиров — никто ничего не слышал. Объявление прозвучало только для него.</p>
  <p id="TchO">А через неделю, поздним вечером, поезд действительно остановился в неположенном месте. Двери открылись, и Семён увидел платформу, которой не должно было быть.</p>
  <p id="Szd6">Табличка гласила: «Тихая». Станция была пустынной, освещённой тусклыми лампами. И там царила такая тишина, какой Семён не слышал никогда.</p>
  <p id="uMot">Это была тишина до начала времён. Тишина небытия. Абсолютная, кристально чистая, почти болезненная в своём совершенстве.</p>
  <p id="pWRT">Семён вышел. Поезд исчез, словно растворился в воздухе.</p>
  <p id="gVb5">На станции больше не было никого, кроме пожилого дежурного в старой форме.</p>
  <p id="4xwj">— Добро пожаловать в архив, — сказал тот. — Я Николай Семёнович, хранитель. Давно вас ждал, коллекционер.</p>
  <p id="Nxon">Оказалось, что станция «Тихая» — это хранилище всей тишины мира. Каждого молчания, каждой паузы, каждого невысказанного слова. Николай Семёнович собирал её уже пятьдесят лет.</p>
  <p id="Aqat">— Но зачем? — спросил Семён, бродя по бесконечным залам, где в стеклянных резервуарах хранились различные виды безмолвия.</p>
  <p id="63eG">— А зачем нужны библиотеки? — ответил старик. — Кто-то же должен сохранять то, что люди теряют. Мир становится всё громче. Скоро настоящей тишины не останется вовсе.</p>
  <p id="Ug2G">В одном зале Семён увидел огромный резервуар с табличкой «Последняя тишина Земли».</p>
  <p id="36D4">— Что это?</p>
  <p id="5QpL">— То, что останется, когда люди замолкнут навсегда, — грустно пояснил Николай Семёнович. — Она уже собрана. Из будущего. Время здесь работает не так, как снаружи.</p>
  <p id="4TTo">Семён провёл на станции несколько часов, изучая коллекцию. Здесь была тишина динозавров, тишина первого человека, тишина между взмахами крыльев последней бабочки вымершего вида. Невероятно.</p>
  <p id="GxxB">— Хотите стать моим помощником? — предложил хранитель. — У вас талант. А мне скоро на пенсию.</p>
  <p id="uOCQ">— Но как же моя жизнь наверху?</p>
  <p id="V4MT">— А какая разница? Здесь у вас будет доступ ко всей тишине мира. Вы сможете изучать её, классифицировать, оберегать от исчезновения.</p>
  <p id="zd65">Семён колебался. Предложение было заманчивым. Но что-то его смущало.</p>
  <p id="1mMI">— А вы сами не скучаете по звукам?</p>
  <p id="gw72">Николай Семёнович задумался:</p>
  <p id="f6UB">— Знаете, после стольких лет в тишине начинаешь понимать… без звука она теряет смысл. Тишина существует только в противовес шуму. Она определяется тем, чего в ней нет.</p>
  <p id="NwS8">— И что вы хотите этим сказать?</p>
  <p id="E30G">— Что, возможно, мы делаем не то. Сохраняем тишину, но убиваем её суть. Коллекционируем безмолвие, но лишаем его жизни.</p>
  <p id="CjCK">В этот момент Семён услышал что-то невероятное — из одного резервуара доносился слабый звук. Плач.</p>
  <p id="jy8O">— Это что?</p>
  <p id="qrLe">— Тишина после похорон, — вздохнул хранитель. — Она… портится. Начинает звучать. Такое иногда случается с самой болезненной тишиной.</p>
  <p id="GR0P">Семён подошёл ближе. И понял: тишина плакала, потому что её разлучили с горем, которое её породило. Она была неполной без слёз, которые должны были её заполнить.</p>
  <p id="4yaZ">— Николай Семёнович, а что, если мы всё делаем неправильно?</p>
  <p id="tcTZ">— В каком смысле?</p>
  <p id="EtBb">— Тишина не должна жить в банках. Она должна жить между звуками. В паузах. В моментах, когда люди замолкают, чтобы услышать своё сердце.</p>
  <p id="hAR8">Старик посмотрел на свою коллекцию новыми глазами:</p>
  <p id="fN7H">— Вы хотите сказать, что мы… крадём тишину у мира?</p>
  <p id="pyp3">— Именно. Каждое безмолвие, которое мы помещаем сюда, больше не может выполнять свою функцию. Помочь человеку подумать, отдохнуть, услышать самого себя.</p>
  <p id="JJuc">Они стояли в архиве украденной тишины и понимали масштаб ошибки.</p>
  <p id="rMoL">— Что же делать? — прошептал Николай Семёнович.</p>
  <p id="F5T9">— Вернуть.</p>
  <p id="nULW">Они открыли все резервуары. Тишина рванула наружу, как вода из прорвавшейся плотины. Тишина снежинок вернулась к зимним вечерам, тишина библиотек — к читальным залам, тишина между ударами сердца — к влюблённым.</p>
  <p id="3fCt">Станция начала растворяться.</p>
  <p id="ZWSx">— А что будет с вами? — крикнул Семён хранителю сквозь нарастающий гул освобождающейся тишины.</p>
  <p id="WqTu">— Я тоже вернусь! — улыбнулся тот. — К своей семье, к звукам настоящей жизни!</p>
  <p id="TSJV">Семён очнулся в вагоне метро. Пассажиры вокруг шумели, разговаривали по телефонам, смеялись. И между звуками, в паузах их речи, жила правильная тишина. Живая, осмысленная, нужная.</p>
  <p id="rxsU">Дома он разобрал свои приборы для ловли безмолвия. Вместо этого стал записывать звуки — смех детей, шум дождя, тихое дыхание спящего города. А тишину оставил там, где она должна быть.</p>
  <p id="aztn">В промежутках между звуками. В секундах раздумий. В мгновениях, когда сердце замирает от красоты.</p>
  <p id="Bmuv">Иногда в метро он слышал объявления о задержках на несуществующих станциях. Но больше никогда не выходил. Теперь он знал: некоторые коллекции лучше не начинать.</p>
  <p id="DOQS">А тишина благодарно окружала его по вечерам, когда он пил чай и слушал, как за окном засыпает мир.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/nedzen</guid><link>https://azhark.cc/nedzen?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/nedzen?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Национальная новостная платформа: когда государство решает, что вам читать</title><pubDate>Tue, 14 Apr 2026 17:21:33 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/67/f7/67f78905-8578-4028-abff-36bc7cf69432.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/53/e8/53e8b626-6953-449a-82e5-497f452eac31.png"></img>Читал сегодня новостную ленту, и одна публикация заставила меня остановиться. Не потому что была неожиданной, а скорее потому что я увидел, как в законопроект превратили то, что давно витало в воздухе.]]></description><content:encoded><![CDATA[
  <figure id="DgLm" class="m_original">
    <img src="https://img2.teletype.in/files/53/e8/53e8b626-6953-449a-82e5-497f452eac31.png" width="1536" />
  </figure>
  <p id="0YUF">Читал сегодня новостную ленту, и одна публикация заставила меня остановиться. Не потому что была неожиданной, а скорее потому что я увидел, как в законопроект превратили то, что давно витало в воздухе.</p>
  <blockquote id="bz1s">«Дзен» претендует на статус Национальной новостной платформы. Виджет с новостями предлагается в обязательном порядке встраивать на главные страницы маркетплейсов, соцсетей и видеосервисов. А какие СМИ попадут в этот виджет — решает правительство.</blockquote>
  <p id="ndbo">Меня это сразило наповал. Не сам факт регулирования — к нему все давно привыкли. А масштаб замысла: взять информационное пространство и перестроить его так, чтобы каждый пользователь рунета, зайдя на любую крупную площадку, видел строго определённую повестку, утверждённую государством. Не рекомендации алгоритма, не выбор редакции, не подписки пользователя, а список, составленный чиновниками.</p>
  <h2 id="что-именно-предлагается">Что именно предлагается</h2>
  <p id="BWon">13 апреля «Коммерсантъ» <a href="https://www.kommersant.ru/doc/8588416" target="_blank">опубликовал материал</a> со ссылкой на внутреннюю презентацию VK. Суть инициативы — поправки в федеральный закон «Об информации», которые могут внести в Госдуму уже в мае 2026 года. Я проверил оригинал и сверил с другими источниками — <a href="https://habr.com/ru/news/1023110/" target="_blank">Хабр</a>, <a href="https://www.fontanka.ru/2026/04/13/76364363/" target="_blank">Фонтанка</a>, <a href="https://www.cnews.ru/news/top/2026-04-14_v_rossii_postroyat_natsionalnuyu" target="_blank">CNews</a>. Всё сходится. Вот ключевые пункты.</p>
  <p id="Wc7j"><strong>Обязательный виджет.</strong> Все поисковые системы, социальные сети, маркетплейсы и видеосервисы должны будут встроить новостной блок «Дзена» на свои главные страницы. В соцсетях и поисковиках — топ-5 новостей. На маркетплейсах и видеоплатформах — кнопка перехода к топ-15 новостям.</p>
  <p id="3Nws"><strong>Порог и сроки.</strong> Площадки с ежедневной аудиторией больше 5 миллионов пользователей обязаны установить виджет сразу после принятия закона. Остальные получат отсрочку на полгода. По сути, это значит — все заметные площадки рунета.</p>
  <p id="0Lyw"><strong>Государственный отбор СМИ.</strong> Какие издания попадут в виджет, определяет правительство РФ. Оно же назначает оператора платформы, причём сразу на пять лет.</p>
  <p id="D2dU"><strong>Распределение денег.</strong> 50 % рекламной выручки идёт площадкам, разместившим виджет. Оставшиеся 50 % — «на поддержку СМИ». Из этой половины 50 % получают конкретные издания, 10 % — некая «ассоциация СМИ». Куда деваются оставшиеся 40 %, в презентации не сказано.</p>
  <h2 id="что-vk-говорит-в-своё-оправдание">Что VK говорит в своё оправдание</h2>
  <p id="wp1t">Аргументация VK выглядит так: с 2022 года аудитория российских СМИ упала на 30–70 %. Медиа «критически зависимы от трафика „Дзена&quot;», который обеспечивает до 80 % переходов на сайты изданий. При этом сам «Дзен» стагнирует с сентября 2022 года, с момента, когда Яндекс продал его VK. Вывод VK: если ничего не делать, закроются негосударственные федеральные СМИ, и рынок монополизируют ТАСС, РИА «Новости» и RT.</p>
  <p id="kQzo">Я даже не знаю, что тут сказать. Компания, которая не смогла развить купленный продукт, превратила его в никому не интересный тухляк, предлагает государству заставить весь рунет этот продукт использовать. А чтобы предложение звучало убедительнее, пугает тем, что без принудительных мер останутся только государственные СМИ (а чем, кстати, это хуже, чем транслировать госновости через Дзен?). То есть решение проблемы государственной монополии на информацию — усилить государственный контроль над информацией.</p>
  <p id="EMfu">Классный подход. Чем-то напоминает «развитие» АвтоВАЗа за счёт гигантского «утильсбора» и помощь Почте России за счёт комиссии с маркетплейсов. И больше всего меня пугает то, что несмотря на очевидную вроде бы несуразность, инициативу могут принять.</p>
  <h2 id="почему-это-важно">Почему это важно</h2>
  <p id="qRp0">Давайте разберём, что здесь происходит на самом деле.</p>
  <p id="Jedt"><strong>Это не спасение медиа.</strong> Если бы целью было поддержать СМИ, достаточно было бы грантовой программы или налоговых льгот. Здесь же строится инфраструктура принудительной дистрибуции контента с единым оператором, назначаемым правительством, и государственным отбором источников.</p>
  <p id="BmMw"><strong>Это не для удобства пользователей.</strong> Ни один пользователь Ozon или Wildberries не приходит туда за новостями. Обязательный виджет — это навязывание контента там, где его не ждут и не хотят. Это не сервис, а инструмент воздействия.</p>
  <p id="jud7"><strong>Это настоящий контроль повестки.</strong> Ключевой пункт всей инициативы — правительство отбирает «правильные» СМИ. Не алгоритм, не пользователь, не редакция площадки. Правительство. Это значит, что из информационного блока, который увидят десятки миллионов людей ежедневно, будут исключены все источники, не вписывающиеся в официальную линию.</p>
  <p id="Mrjn"><strong>Это способ увеличить доход VK.</strong> «Дзен» стагнирует, и VK нашла элегантный способ решить коммерческую проблему за счёт законодательного принуждения. Заставить конкурентов — маркетплейсы, соцсети, видеосервисы — бесплатно отдавать трафик «Дзену», да ещё и делиться рекламной выручкой, 40 % которой растворяются в недосказанности презентации.</p>
  <h2 id="контекст-это-не-первый-шаг">Контекст: это не первый шаг</h2>
  <p id="fycY">Эту инициативу нельзя рассматривать изолированно. Она ложится в ряд последовательных шагов по ужесточению контроля над информационным пространством: замедление и блокировки платформ, закон о «приземлении», реестры блогеров и прочие запретительные меры, количество которых уже сложно даже посчитать. Каждый следующий шаг выглядит «логичным продолжением» предыдущего, и каждый сужает пространство для независимой информации.</p>
  <p id="smWo">Национальная новостная платформа — это следующий уровень. Не просто блокировка нежелательного, а принудительная трансляция желательного. Разница принципиальная: одно дело — убрать из поля зрения то, что не нравится власти. Другое — заставить каждую крупную площадку транслировать только то, что власть считает нужным.</p>
  <h2 id="вывод">Вывод</h2>
  <p id="bJ5P">Перед нами проект, в котором коммерческие интересы VK идеально совпали с желанием государства контролировать информацию. VK получает принудительный трафик и рекламные деньги. Государство получает единый канал доставки утверждённой повестки на все крупные площадки рунета. Пользователь получает виджет, который он не просил, с новостями, которые отобрали за него.</p>
  <p id="KpxS">Самое тревожное — не конкретный законопроект, а направление движения. Когда государство решает, что вам читать, где вам это читать и через какой единственный канал это до вас дойдёт, — это уже не регулирование медиарынка. Это конструирование информационной реальности.</p>
  <hr />
  <p id="ttmw"><em>Источники:</em></p>
  <ul id="SH2R">
    <li id="5sSl"><a href="https://www.kommersant.ru/doc/8588416" target="_blank">Коммерсантъ — «Дзен» претендует на статус Национальной новостной платформы</a></li>
    <li id="9feA"><a href="https://habr.com/ru/news/1023110/" target="_blank">Хабр — На базе «Дзена» предложено создать «Национальную информационную платформу»</a></li>
    <li id="7kTE"><a href="https://www.fontanka.ru/2026/04/13/76364363/" target="_blank">Фонтанка — Национальная новостная платформа на базе «Дзена»</a></li>
    <li id="MUrx"><a href="https://www.cnews.ru/news/top/2026-04-14_v_rossii_postroyat_natsionalnuyu" target="_blank">CNews — В России построят национальную новостную платформу</a></li>
    <li id="CxH4"><a href="https://www.retail.ru/news/marketpleysy-mogut-zastavit-delitsya-trafikom-s-dzenom-14-aprelya-2026-276654/" target="_blank">Retail.ru — Маркетплейсы могут заставить делиться трафиком с «Дзеном»</a></li>
  </ul>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/superstitions</guid><link>https://azhark.cc/superstitions?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/superstitions?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Суеверия рационального человека</title><pubDate>Mon, 13 Apr 2026 18:45:46 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a4/a1/a4a1827a-d3e2-498b-8b7d-e12c4b6aa391.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/ac/79/ac79661d-1048-4819-aea0-d010c1c66940.png"></img>Я не суеверный человек. Почти не верю в гороскопы, не ношу красную нить, не плюю через плечо. Но если у меня всё идёт хорошо, я постараюсь не говорить об этом вслух.]]></description><content:encoded><![CDATA[
  <figure id="1rpD" class="m_original">
    <img src="https://img3.teletype.in/files/ac/79/ac79661d-1048-4819-aea0-d010c1c66940.png" width="1536" />
  </figure>
  <p id="IJLo">Я не суеверный человек. Почти не верю в гороскопы, не ношу красную нить, не плюю через плечо. Но если у меня всё идёт хорошо, я постараюсь не говорить об этом вслух.</p>
  <p id="PhAn">И вот тут, если вы честны с собой, вы только что кивнули. Или хотя бы поёжились. Потому что это самое распространённое суеверие образованных людей — то, в котором мы себе не признаёмся. Мы смеёмся над чёрными кошками, считаем астрологию чушью, иронизируем над приметами — и при этом живём по набору правил, которые ничем принципиально не отличаются от бабушкиного «не свисти — денег не будет». Просто наши правила звучат приличнее.</p>
  <p id="DJBr">Рациональные люди не свободны от суеверий. Они просто маскируют их под «привычки», «осторожность» и «здравый смысл». Механизм тот же — магическое мышление: вера в то, что определённые действия или слова влияют на реальность способом, который невозможно объяснить причинно-следственной связью. Обёртка другая, содержимое то же.</p>
  <h2 id="сглазить">Сглазить</h2>
  <p id="t0le">Страх сглазить — пожалуй, самое живучее суеверие в арсенале современного человека с высшим образованием. Оно не выглядит как суеверие. Оно выглядит как воспитание.</p>
  <p id="7Iv1">Не рассказывать о планах, пока не сбылись. Не хвастаться здоровьем. Не говорить «у нас всё отлично» — а отвечать «нормально», «потихоньку», «грех жаловаться». На вопрос «как дела?» — что угодно, только не правду, если правда хорошая. Формально — скромность. Привычка. Осторожность. Фактически — глубокая, неартикулированная уверенность, что вселенная подслушивает и наказывает за самоуверенность. Сказал «всё хорошо» — и вот, пожалуйста: словно кто-то наверху услышал и решил это исправить.</p>
  <p id="v5o1">Примеры — на каждом шагу, стоит только начать замечать. Беременность не обсуждают до двенадцати недель. Медицинское обоснование для этого есть: первый триместр действительно рискованный. Но давайте будем честны — большинство людей боятся не статистики выкидышей. Они боятся сглазить. При увольнении скрывают новое место работы, потому что позавидуют. Сделку не обсуждают до подписания, и дело не в коммерческой тайне, а в смутном ощущении, что слова способны разрушить то, что ещё не оформилось. «Тьфу-тьфу-тьфу» произносится на автопилоте, машинально, как рефлекс — и именно поэтому оно показательно. Суеверие, которое настолько впиталось, что уже не воспринимается как суеверие.</p>
  <p id="aGku">Обратите внимание на язык: мы не говорим «я боюсь, что не получится». Мы говорим «не хочу загадывать». Первое — признание тревоги. Второе — рациональная осторожность. Разница в формулировке, но не в том, что за ней стоит. Под «не буду загадывать» лежит тот же древний, довербальный страх: не дразни судьбу. Просто он переодет в деловой костюм.</p>
  <h2 id="ритуал-подготовки-как-оберег">Ритуал подготовки как оберег</h2>
  <p id="OiPX">«Если я возьму зонт, дождя не будет». Фраза, которую каждый произносил хотя бы раз, и не всегда в шутку.</p>
  <p id="M3LB">Это не наблюдение. Это заклинание.</p>
  <p id="BWbL">Рациональная интерпретация, конечно, существует: когда берёшь зонт, дождь не мешает, поэтому субъективно кажется, что его не было. Всё объяснимо, никакой мистики. Но спросите любого знакомого, и он, чуть замявшись, признает: дело не только в этом. Многие честно верят в причинно-следственную связь: подготовка к плохому <em>предотвращает</em> плохое.</p>
  <p id="ZPpz">Купил страховку, значит, авария не случится. Положил аптечку в машину, значит, не понадобится. Подготовился к худшему сценарию на переговорах, значит, он не наступит. Взял зарядку — телефон не сядет. Выучил все билеты перед экзаменом — спросят именно тот, который не учил. Но если <em>все</em> выучил, если подготовился <em>по-настоящему</em>, вселенная это заметит и смилостивится.</p>
  <p id="HwrC">Это инверсия контроля. Мы не можем предотвратить событие напрямую и совершаем ритуал, который создаёт <em>иллюзию</em> влияния. Подготовка перестаёт быть практическим действием и становится магическим жестом: я сделал правильную вещь, значит, плохое отступит. Я принёс жертву — своим временем, деньгами, вниманием — и теперь имею право на благоприятный исход. Не потому что подготовка помогает, а потому что она <em>заслужена</em>.</p>
  <p id="0vmw">Логика та же, что у шамана, рисующего бизона на стене пещеры перед охотой. Он тоже «готовился». И если его спросить, он бы, вероятно, объяснил это не магией, а «настройкой перед делом» — и мы бы снисходительно кивнули, понимая, что это суеверие. А потом пошли бы собирать аптечку в дорогу, тихо надеясь, что сам факт её наличия защитит от необходимости ею воспользоваться.</p>
  <h2 id="вещи-которые-работают">Вещи, которые «работают»</h2>
  <p id="HaZS">У каждого есть вещи, которые «работают». Любимая кружка, с которой почему-то лучше работается. Определённый плейлист для сосредоточенности — не фоновый шум, а именно этот, конкретный, проверенный. Кресло в кафе — не то, что у окна, а вот то, в углу, с потёртой обивкой, и если оно занято — день уже чуть хуже. Привычный маршрут до магазина, хотя есть короче. Конкретная ручка, футболка, место за столом. Сторона кровати, порядок, в котором складываются вещи в рюкзак.</p>
  <p id="lz6g">Рациональный человек объяснит это «привычкой» и «якорением» и будет отчасти прав. Привычное окружение снижает когнитивную нагрузку, мозг не тратит ресурсы на ориентирование и может сосредоточиться на задаче. Всё логично. Нейронаука подтвердит: стабильный контекст облегчает вход в рабочее состояние. Никакой магии.</p>
  <p id="7zQH">Но если кружка разобьётся, этот же рациональный человек расстроится несоразмерно потере предмета стоимостью пятьсот рублей. Не из-за денег. Не из-за неудобства — можно купить такую же за полчаса. Расстроится потому, что кружка была тотемом. Она нагружена значением, которое не имеет отношения к керамике и объёму. Это не сосуд для чая, это «вещь, с которой получается». И заменить её нельзя, потому что «такая же» — это не <em>та самая</em>.</p>
  <p id="x1pI">Отличие от религиозного амулета ровно одно: мы знаем «правильное» объяснение. Мы можем сказать «это якорение, описанное в когнитивной психологии». Но поведение то же самое. Утрата вещи вызывает тревогу, несоразмерную её стоимости. Замена не годится, даже если объективно идентична. Мы ведём себя так, будто в предмете живёт что-то, чего нет в его копии. Это и есть магическое мышление, просто мы называем его «ассоциацией».</p>
  <h2 id="почему-умные-люди-не-защищены">Почему умные люди не защищены</h2>
  <p id="11ud">Можно было бы списать всё это на культурный багаж, привычки, воспитание. Но когнитивная наука показывает кое-что менее утешительное: дело не в культуре, а в устройстве мозга. Механизмы, стоящие за суевериями, встроены в саму архитектуру мышления. Предвзятость подтверждения — мы запоминаем совпадения и забываем несовпадения. Один раз не взял зонт и попал под дождь — запомнил на годы. Двадцать раз не взял и не попал — не заметил. Иллюзия контроля — Эллен Лангер в 1975 году показала, что люди ведут себя так, будто могут повлиять на бросок кубика: ставки увереннее, если бросаешь сам, а не наблюдаешь. Апофения — термин психиатра Клауса Конрада — способность видеть паттерны и связи там, где их объективно нет. Три хороших дня подряд после покупки нового ежедневника — и вот уже ежедневник «работает».</p>
  <p id="svFn">Даниэль Канеман в «Думай медленно… решай быстро» разложил это на две системы. Быстрое, интуитивное мышление — Система 1. Медленное, аналитическое — Система 2. Суеверие — продукт первой. Она видит корреляцию и мгновенно, без усилий делает вывод о причинности. Надел «счастливую» рубашку — собеседование прошло хорошо — значит, рубашка помогла. Вторая система могла бы поправить, но ей лень: она энергозатратна и включается только когда мы специально заставляем себя думать критически. А кто будет критически думать о рубашке?</p>
  <p id="7Zaq">И вот ключевое, ради чего вся эта когнитивная преамбула: интеллект и образование не защищают от этих искажений. Нисколько. Они дают лишь более изощрённые рационализации.</p>
  <p id="nfGZ">Глупый человек стучит по дереву и знает, что это суеверие. Умный стучит по дереву и объясняет это «мышечной привычкой», «социальным ритуалом, который я выполняю иронически», или «способом снизить тревожность через физическое действие». Объяснение красивее. Результат тот же — он стучит.</p>
  <p id="NdKz">Более того: чем умнее человек, тем изощрённее защита. Человек с одним высшим стучит по дереву со смущённой улыбкой. Человек с двумя объясняет это теорией ожиданий и управлением когнитивной нагрузкой. Человек с кандидатской степенью стучит по дереву, а потом пишет об этом рефлексивное эссе. Глубина понимания растёт. Деревянная поверхность остаётся той же.</p>
  <h2 id="суеверие-эффективности">Суеверие эффективности</h2>
  <p id="lwj0">Есть целый пласт суеверий, замаскированный настолько хорошо, что его принято считать не просто нормой, а добродетелью. Наша эпоха возвела в культ веру в ритуалы продуктивности, и мало кто замечает, что это именно вера.</p>
  <p id="6qx5">Правильное утро: подъём в 5:30, холодный душ, медитация, дневник благодарности, стакан воды с лимоном. Правильный ежедневник — не любой, а конкретной системы, с правильной разметкой. Правильная ручка — не шариковая за тридцать рублей, а та, которой «приятно писать». Правильное приложение для задач — не то, которое удобно, а то, в которое веришь. Правильная музыка. Правильное рабочее место. Правильная последовательность.</p>
  <p id="cWOi">Если день не задался, первая мысль: я нарушил ритуал. Проснулся позже, пропустил медитацию, не записал цели на день — и вот, пожалуйста, всё пошло наперекосяк. Причинно-следственная связь очевидна. Для Системы 1.</p>
  <p id="ZbEN">Для Системы 2, если её включить, — нет. День пошёл наперекосяк, потому что заболел ребёнок, упал сервер или позвонил сложный клиент. Ни одно из этих событий не связано с тем, медитировал ты утром или нет. Но связь ощущается настолько реальной, что сомневаться в ней неприятно.</p>
  <p id="NMVG">Это буквально магическое мышление: определённая последовательность действий вызывает определённый результат, даже если причинной связи между ними нет. Но ощущение контроля — «я сделал всё правильно» — настолько ценно, что мы цепляемся за него, как средневековый крестьянин за молитву перед посевом.</p>
  <p id="lOU2">Причём ритуал устроен хитро: если день удался, значит, ритуал помог. Если не удался, значит, ритуал нарушил или выполнил недостаточно тщательно. Система нефальсифицируема. Это не наука. Это именно вера со своими священными текстами (книги про привычки), пророками (авторы курсов продуктивности) и обрядами (утренние рутины).</p>
  <p id="Tjbk">Индустрия продуктивности продаёт не инструменты. Она продаёт амулеты. Красивые, хорошо упакованные, с научными ссылками на обложке, но по сути амулеты. «Делай вот это каждое утро, и жизнь наладится.» Формула заклинания, прикрытая языком лайфхаков. Отличие от молитвенника только в шрифте и цене подписки.</p>
  <h2 id="полезное-суеверие">Полезное суеверие</h2>
  <p id="V4gB">Было бы удобно на этом закончить, мол, мы все немного суеверны, давайте это признаем и посмеёмся. Но есть финальный поворот, который всё усложняет.</p>
  <p id="OrXj">Некоторые суеверия действительно «работают». Не в смысле «влияют на реальность», а в смысле «влияют на нас, а мы влияем на реальность». Это важное различие, и оно всё запутывает.</p>
  <p id="5qez">Плацебо — не «самовнушение для слабых». Это реальный, измеримый, воспроизводимый эффект. Если ритуал снижает тревожность, он объективно улучшает результат. Лисанн Дамиш с коллегами в 2010 году провели серию экспериментов: участники, которым давали «счастливый» мяч для гольфа или говорили «скрести пальцы», показывали лучшие результаты в моторных и когнитивных задачах. Не потому, что мяч волшебный. Потому что ритуал повышал уверенность в себе и снижал тревожность, а это уже влияло на результат.</p>
  <p id="8H4D">Спортсмены с предстартовыми ритуалами выступают лучше. Это не магия, а переключение фокуса с тревоги на действие. «Счастливая» кружка действительно помогает сосредоточиться через якорение. Утренний ритуал действительно задаёт тон дня через снижение неопределённости. Даже пресловутое «тьфу-тьфу-тьфу» выполняет функцию: отмечает тревогу, признаёт её и символически закрывает вместо того чтобы позволить ей расти в фоновом режиме.</p>
  <p id="WL7e">Суеверие работает не как магия, а как хак нервной системы. Обход рационального контроля, который приносит реальный, измеримый результат. Мы обманываем себя, и этот обман полезен. Причём, что интересно, плацебо работает даже когда пациент знает, что принимает пустышку: так называемый «открытый плацебо» всё равно даёт эффект. Мозгу, похоже, достаточно самого ритуала даже если рациональная часть отдаёт себе отчёт в происходящем.</p>
  <p id="BaRg">И тут возникает парадокс, из-за которого всю эту конструкцию нельзя просто разоблачить и отбросить.</p>
  <p id="qmae">Если суеверие работает через плацебо, а мы знаем, что это плацебо, оно всё ещё суеверие? Или уже инструмент? Если я стучу по дереву, понимаю, что это не влияет на реальность, но чувствую, как снижается тревога, я суеверный или практичный? Если кружка помогает мне работать и я знаю почему, это магическое мышление или осознанное использование когнитивных механизмов?</p>
  <p id="mw9Y">Граница между суеверием и инструментом проходит, видимо, через осознанность. Суеверие — это когда веришь, что ритуал влияет на мир. Инструмент — когда понимаешь, что ритуал влияет на тебя. Звучит чётко. На практике же размыто до невозможности.</p>
  <p id="q1tR">Потому что осознанность — штука ненадёжная. Сегодня я «иронически» стучу по дереву. Завтра забываю про иронию. Послезавтра ловлю себя на искреннем беспокойстве, когда деревянной поверхности поблизости нет. Осознанность — не выключатель, а диммер, и его ручка постоянно ползёт сама.</p>
  <p id="euMu">А главное — вопрос, с которого всё начиналось: если я всё это знаю и понимаю, — почему мне всё равно не хочется говорить вслух, что всё идёт хорошо?</p>
  <p id="QPwK">Может быть, рациональный человек отличается от суеверного не отсутствием суеверий, а качеством объяснений, почему его суеверия «не считаются».</p>
  <p id="JdAx">Тьфу-тьфу-тьфу.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://azhark.cc/compression</guid><link>https://azhark.cc/compression?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark</link><comments>https://azhark.cc/compression?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=azhark#comments</comments><dc:creator>azhark</dc:creator><title>Сжатие данных: от кода Хаффмана до zstd</title><pubDate>Sun, 12 Apr 2026 17:58:11 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/59/15/59151e60-91ae-4a6e-8877-6b2ce3418ca0.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/93/10/93109d07-8c15-43fa-86e3-84fd03141558.png"></img>Текст «Войны и мира» в UTF-8 занимает около 3,2 МБ. Команда gzip сжимает его до 1,2 МБ — почти втрое. HTML-страница типичного сайта проходит через Brotli и уменьшается в 5–7 раз, прежде чем браузер получит первый байт. Образ Docker, ядро Linux, бэкап базы данных — всё хранится и передаётся в сжатом виде. Сжатие настолько вездесуще, что мы его не замечаем.]]></description><content:encoded><![CDATA[
  <figure id="rnqo" class="m_original">
    <img src="https://img2.teletype.in/files/93/10/93109d07-8c15-43fa-86e3-84fd03141558.png" width="1536" />
  </figure>
  <p id="tRzJ">Текст «Войны и мира» в UTF-8 занимает около 3,2 МБ. Команда <code>gzip</code> сжимает его до 1,2 МБ — почти втрое. HTML-страница типичного сайта проходит через Brotli и уменьшается в 5–7 раз, прежде чем браузер получит первый байт. Образ Docker, ядро Linux, бэкап базы данных — всё хранится и передаётся в сжатом виде. Сжатие настолько вездесуще, что мы его не замечаем.</p>
  <p id="3Xfv">Но за ним стоит красивая теория с жёстким ограничением: существует предел, за которй не может перешагнуть ни один алгоритм сжатия данных. Понимание этого предела — ключ к пониманию того, почему одни данные сжимаются в десятки раз, а другие не сжимаются вовсе, и почему gzip, придуманный в 1992 году, до сих пор работает на большинстве серверов планеты.</p>
  <h2 id="простейшая-идея-сжимать-повторы">Простейшая идея: сжимать повторы</h2>
  <p id="czTG">Интуиция подсказывает: если в данных есть повторяющиеся фрагменты, их можно записать короче. Самый наивный подход — <strong>RLE</strong> (<em>Run-Length Encoding</em>), кодирование длин серий: вместо <code>AAAAAABBB</code> написать <code>6A3B</code>.</p>
  <pre id="Xhhs">// RLE: кодирование длин серий
fn rle_encode(data: &amp;[u8]) -&gt; Vec&lt;u8&gt; {
    let mut result = Vec::new();
    let mut i = 0;
    while i &lt; data.len() {
        let ch = data[i];
        let mut count = 1u8;
        while i + (count as usize) &lt; data.len()
            &amp;&amp; data[i + (count as usize)] == ch
            &amp;&amp; count &lt; 9
        {
            count += 1;
        }
        result.push(b&#x27;0&#x27; + count);
        result.push(ch);
        i += count as usize;
    }
    result
}
</pre>
  <pre id="0xld">Вход:  &quot;AAABBBCCDDDDDDEEEF&quot; (18 байт)
RLE:   &quot;3A3B2C6D3E1F&quot;       (12 байт, 67 %)
</pre>
  <p id="mbWx">RLE работает отлично на данных с длинными сериями одинаковых байт, например, на изображениях с большими однотонными областями. Но на обычном тексте он бесполезен: в слове <code>&quot;hello&quot;</code> нет длинных повторов, и RLE только увеличит размер. Нужен другой подход.</p>
  <h2 id="энтропия-сколько-информации-в-ваших-данных">Энтропия: сколько информации в ваших данных</h2>
  <p id="ELck">В 1948 году Клод Шеннон опубликовал статью <em>«A Mathematical Theory of Communication»</em> и ввёл понятие, которое определило всю дальнейшую теорию сжатия — <strong>энтропию</strong>:</p>
  <pre id="dI09">H = −Σ P(xᵢ) × log₂(P(xᵢ))
</pre>
  <p id="SFkr">Энтропия H — среднее количество бит информации на символ. Что это значит на практике? Посчитаем:</p>
  <pre id="bxya">fn shannon_entropy(data: &amp;[u8]) -&gt; f64 {
    let mut freq = [0usize; 256];
    for &amp;b in data {
        freq[b as usize] += 1;
    }
    let len = data.len() as f64;
    let mut h = 0.0;
    for &amp;f in &amp;freq {
        if f &gt; 0 {
            let p = f as f64 / len;
            h -= p * p.log2();
        }
    }
    h
}
</pre>
  <pre id="i58y">&quot;aaaaaaaaaa&quot;    H = 0.000 бит/символ
&quot;aababcabcd&quot;    H = 1.846 бит/символ
&quot;hello world&quot;   H = 2.845 бит/символ
&quot;abcdefghij&quot;    H = 3.322 бит/символ
</pre>
  <p id="V6cM">Строка из одних <code>a</code> имеет энтропию 0. Зная первый символ, вы знаете все остальные. Строка из десяти различных символов имеет самую большую энтропию для этого алфавита: log₂(10) ≈ 3,32 бита. Каждый символ несёт максимум информации, предсказать следующий невозможно.</p>
  <p id="5fFX"><strong>Теорема Шеннона о кодировании источника</strong> утверждает: никакой алгоритм без потерь не может сжать данные ниже их энтропии. Это фундаментальный предел, как скорость света для передачи сигнала.</p>
  <p id="1Tuu">Английский текст имеет энтропию около 1–1,5 бит на символ (с учётом частотности пар и троек букв). ASCII использует 8 бит на символ. Разница — 5–7 крат — это и есть теоретический запас для сжатия.</p>
  <h2 id="код-хаффмана-частые-символы--короткие-коды">Код Хаффмана: частые символы — короткие коды</h2>
  <p id="Xl20">В 1952 году студент MIT Дэвид Хаффман получил от профессора Роберта Фано задание на курсовую: придумать способ закодировать набор символов двоичными последовательностями так, чтобы итоговое сообщение занимало как можно меньше бит. Фано сам работал над этим совместно с Шенноном, и их метод (Shannon–Fano coding) давал хорошие, но не оптимальные результаты. Хаффман нашёл решение, строя кодовое дерево снизу вверх — от наименее частых символов к наиболее частым.</p>
  <p id="k884">Идея: частым символам — короткие коды, редким — длинные. Возьмём строку <code>&quot;abracadabra&quot;</code>:</p>
  <pre id="kF5z">// Частотный анализ &quot;abracadabra&quot;
let text = b&quot;abracadabra&quot;;
let mut freq: HashMap&lt;u8, usize&gt; = HashMap::new();
for &amp;b in text.iter() {
    *freq.entry(b).or_insert(0) += 1;
}
</pre>
  <pre id="2MZ0">&#x27;a&#x27;: 5  (45 %)  →  код: 0       (1 бит)
&#x27;b&#x27;: 2  (18 %)  →  код: 10      (2 бита)
&#x27;r&#x27;: 2  (18 %)  →  код: 110     (3 бита)
&#x27;c&#x27;: 1  ( 9 %)  →  код: 1110    (4 бита)
&#x27;d&#x27;: 1  ( 9 %)  →  код: 1111    (4 бита)
</pre>
  <p id="JYhQ">В ASCII каждый символ занимает 8 бит, и <code>&quot;abracadabra&quot;</code> — это 88 бит. С кодом Хаффмана: 5×1 + 2×2 + 2×3 + 1×4 + 1×4 = 23 бита. Сжатие в 3,8 раза.</p>
  <p id="Tiz7">Ключевое свойство — <strong>префиксность</strong>: ни один код не является началом другого. Код <code>0</code> (для <code>a</code>) не начинает код <code>10</code> (для <code>b</code>) — потому что <code>0</code> это <em>полный</em> код, а <code>10</code> начинается с <code>1</code>. Благодаря этому поток бит можно декодировать однозначно, без разделителей: <code>010011011101111010</code> → <code>a b r a c a d a b r a</code>.</p>
  <p id="kLq7">Код Хаффмана оказался строго оптимальным среди префиксных кодов. Работу засчитали как курсовую с освобождением от финального экзамена.</p>
  <h2 id="lz77-сжимать-не-символы-а-повторы-подстрок">LZ77: сжимать не символы, а повторы подстрок</h2>
  <p id="k1mO">Алгоритм Хаффмана работает с частотами отдельных символов. Но в реальных данных избыточность проявляется на уровне целых фрагментов: повторяются слова, конструкции, паттерны. В 1977 году Абрахам Лемпель и Яков Зив предложили принципиально другой подход.</p>
  <p id="PXQs">Алгоритм LZ77 поддерживает <strong>скользящее окно</strong> — буфер уже обработанных данных. Когда в потоке встречается фрагмент, который уже есть в окне, вместо самого фрагмента записывается ссылка: «вернись на N байт назад и скопируй M байт».</p>
  <pre id="yvRT">Вход: A B C A B C A B C
           ───────
           совпадение с позиции 0, длина 6

Выход: A B C (ссылка: смещение=3, длина=6)
</pre>
  <p id="BN0E">Девять байт превращаются в три литерала и одну ссылку. На длинных текстах с повторяющимися словами и фразами это даёт серьёзное сжатие.</p>
  <p id="ySSA">LZ77 не требует предварительного анализа всех данных, он работает потоково, обрабатывая вход слева направо. В 1978 году Лемпель и Зив опубликовали LZ78, использующий явный словарь вместо окна. На основе LZ78 Терри Уэлч создал LZW (1984), ставший основой GIF. LZW был запатентован, что в итоге привело к созданию формата PNG как свободной альтернативы.</p>
  <h2 id="deflate-объединяя-два-подхода">DEFLATE: объединяя два подхода</h2>
  <p id="yuXx">Следующий логичный шаг — совместить оба метода. Именно это сделал Фил Кац, программист из Милуоки, в алгоритме DEFLATE для своего архиватора PKZIP 2.0:</p>
  <ol id="sRqy">
    <li id="q2GV"><strong>LZ77</strong> находит повторяющиеся подстроки и заменяет их ссылками.</li>
    <li id="dB1M"><strong>Код Хаффмана</strong> кодирует получившийся поток — и литералы, и ссылки — присваивая частым элементам короткие коды.</li>
  </ol>
  <p id="9V5l">В итоге получается двухступенчатая схема: сначала устраняем повторы на уровне подстрок, потом оптимизируем кодирование оставшегося. Каждый этап ловит свой вид избыточности.</p>
  <p id="MEOR">DEFLATE стал основой трёх популярных форматов: <strong>gzip</strong> (1992) для HTTP и логов, <strong>ZIP</strong> для архивов, <strong>PNG</strong> для изображений. Жан-лу Гайи и Марк Адлер реализовали его в библиотеке <strong>zlib</strong> (1995), которая стала одной из самых распространённых библиотек в истории. Спецификация зафиксирована в RFC 1951 (1996).</p>
  <p id="Xw3W">Сжатие gzip сегодня выполняется в одну строка кода:</p>
  <pre id="9dR2">// Go: сжатие gzip из стандартной библиотеки
data := []byte(strings.Repeat(&quot;Hello, World! &quot;, 500)) // 7000 байт

var buf bytes.Buffer
w := gzip.NewWriter(&amp;buf)
w.Write(data)
w.Close()

// Результат: 7000 → 63 байт (0.9 %)
</pre>
  <pre id="NWu2">// Уровни сжатия: скорость vs степень
// BestSpeed (1):        7000 → 65 байт
// Default (6):          7000 → 63 байт
// BestCompression (9):  7000 → 63 байт
</pre>
  <p id="2wYx">Обратите внимание: разница между уровнями 1 и 9 по размеру — всего 2 байта, а по времени — в разы. Уровень по умолчанию выбран не случайно.</p>
  <h2 id="теоретический-потолок-почему-универсальный-компрессор-невозможен">Теоретический потолок: почему универсальный компрессор невозможен</h2>
  <p id="WibL">Раз DEFLATE сжимает текст в десятки раз, может, можно сжать ещё? А потом сжать результат? И так до бесконечности?</p>
  <p id="1Q9P">Нет. И доказательство элементарно.</p>
  <p id="P4Nf">Строк длины n бит существует ровно 2^n. Строк длины <em>меньше</em> n бит — всего 2^n − 1. По принципу Дирихле невозможно каждой строке длины n сопоставить уникальную строку меньшей длины — «ящиков» не хватает. <strong>Любой алгоритм, который сжимает одни строки, обязан увеличивать другие.</strong></p>
  <p id="3i08">В 1960-х годах три учёных независимо пришли к одному и тому же выводу. Рэй Соломонофф (1960), Андрей Колмогоров (1965) и Грегори Хейтин (1969) определили <em>алгоритмическую сложность</em> строки как длину кратчайшей программы, которая её порождает. Из этого определения следует: большинство строк несжимаемы. Случайная строка с высокой вероятностью не содержит закономерностей, которые можно было бы использовать, и это, собственно, формальное определение случайности.</p>
  <p id="qJ7B">Хейтин описал результат так: «Берём теорию информации Шеннона и теорию вычислимости Тьюринга, помещаем в шейкер для коктейлей и энергично перемешиваем».</p>
  <p id="DrQt">На практике это значит: хорошо сжимается то, что содержит закономерности (текст, код, логи, изображения с плавными градиентами). Случайные данные — шифрованные файлы, хорошо сжатые архивы, белый шум — не сжимаются вообще. Попытка сжать уже сжатый файл обычно <em>увеличивает</em> его размер (из-за метаданных формата).</p>
  <h2 id="zip-бомбы-сжатие-как-оружие">ZIP-бомбы: сжатие как оружие</h2>
  <p id="htaI">Ограничение DEFLATE — максимальная степень сжатия не более 1032:1. Но формат ZIP позволяет вкладывать архивы в архивы. Классическая ZIP-бомба <code>42.zip</code> использует именно это: файл размером 42 КБ содержит пять уровней вложенных ZIP-архивов, по 16 файлов на каждом уровне. На нижнем уровне каждый файл распаковывается до 4,3 ГБ. Итого после полной распаковки — ~4,5 петабайт. Цель — вывести из строя антивирус или любую программу, которая рекурсивно распаковывает архивы.</p>
  <p id="U2Xv">Защита казалась простой: ограничить глубину рекурсивной распаковки. Но в 2019 году Дэвид Фифилд представил на конференции USENIX WOOT <strong>нерекурсивную</strong> ZIP-бомбу. Его метод использует особенность формата ZIP: несколько файловых записей в центральном каталоге могут ссылаться на один и тот же блок сжатых данных. Результат — квадратичный рост размера при распаковке за один проход, без вложенности.</p>
  <pre id="Jx5P">42 КБ   → 5,5 ГБ   (степень 129 000:1)
10 МБ   → 281 ТБ   (степень 28 000 000:1)
46 МБ   → 4,5 ПБ   (с расширениями Zip64)
</pre>
  <p id="9WhO">Всё это — стандартный DEFLATE, один уровень вложенности, никакой рекурсии. Знание того, как работает сжатие, позволяет и защищаться, и атаковать.</p>
  <h2 id="от-gzip-к-zstd-смена-поколений">От gzip к zstd: смена поколений</h2>
  <p id="c2wM">gzip, созданный Гайи и Адлером в 1992 году, продержался стандартом HTTP-сжатия и сжатия логов тридцать лет. Он прост, надёжен и поддерживается буквально везде. Но за это время процессоры изменились неузнаваемо, а алгоритм — нет. Появились замены.</p>
  <h3 id="zstandard-янн-колле-2016">Zstandard (Янн Колле, 2016)</h3>
  <p id="TXY8">Янн Колле, инженер Meta, создал zstd — алгоритм на основе словарного сжатия и конечных автоматов (FSE — <em>Finite State Entropy</em>), оптимизированный под параллельное исполнение на современных CPU. При том же уровне сжатия zstd в 3–5 раз быстрее gzip. При той же скорости сжимает лучше.</p>
  <p id="2sn0">Внедрение шло быстро:</p>
  <ul id="JsZf">
    <li id="crES">Linux kernel 4.14 (2017) — сжатие модулей;</li>
    <li id="6Qh8">Arch Linux (2020) — переход с xz на zstd для пакетов;</li>
    <li id="zuZ1">AWS S3 — внутреннее сжатие (~30 % экономия на экзабайтах данных);</li>
    <li id="zSSD">Chrome 123 и Firefox 126 (2024) — <code>Content-Encoding: zstd</code>;</li>
    <li id="9n5v">; Windows 11 (октябрь 2023) — поддержка в Проводнике.</li>
  </ul>
  <p id="NBlp">Колле также создал и алгоритм <strong>LZ4</strong>, оптимизированный под скорость декомпрессии: более 4 ГБ/с на одном ядре. LZ4 используется в ядре Linux, ZFS, десятках баз данных. Он же создал и xxHash. Забавный факт: LZ4 начинался как побочный проект, когда Колле работал в маркетинговом отделе телеком-компании Orange.</p>
  <h3 id="brotli-google-2015">Brotli (Google, 2015)</h3>
  <p id="nnGu">Жирки Алакуйала и Золтан Сабадка из Google создали Brotli изначально для сжатия веб-шрифтов (WOFF2), а затем обобщили для HTTP. Ключевая идея — предопределённый словарь из часто встречающихся фрагментов HTML, CSS и JavaScript. Для статических ресурсов Brotli даёт лучшую степень сжатия, чем gzip и zstd, но медленнее на высоких уровнях компрессии.</p>
  <p id="URcV">По данным HTTP Archive (январь 2024): Brotli и gzip используются примерно поровну (~40 % каждый), zstd растёт (~2 %), около 11 % ответов не сжаты.</p>
  <h2 id="сжатие-в-go-rust-и-zig">Сжатие в Go, Rust и Zig</h2>
  <h3 id="go-всё-в-стандартной-библиотеке">Go: всё в стандартной библиотеке</h3>
  <p id="Kbt1">Go включает <code>compress/gzip</code>, <code>compress/zlib</code>, <code>compress/flate</code> (DEFLATE), <code>compress/lzw</code> и <code>compress/bzip2</code> (только декомпрессия). Для продакшена часто используют <code>klauspost/compress</code>, совместимый по API, но в 2–5 раз быстрее. Этот же пакет содержит чистую реализацию zstd на Go.</p>
  <pre id="dqOP">import &quot;compress/gzip&quot;

// Сжатие
var buf bytes.Buffer
w := gzip.NewWriter(&amp;buf)
w.Write(data)
w.Close()

// Распаковка
r, _ := gzip.NewReader(&amp;buf)
io.Copy(os.Stdout, r)
</pre>
  <h3 id="rust-экосистема-крейтов">Rust: экосистема крейтов</h3>
  <p id="sGmN">Крейт <code>flate2</code> — обёртка над DEFLATE с четырьмя бэкендами: <code>miniz_oxide</code> (чистый Rust, по умолчанию), <code>zlib-rs</code> (чистый Rust, самый быстрый), <code>zlib-ng</code> (C через FFI) и <code>cloudflare_zlib</code>.</p>
  <p id="PqwF">Отдельного упоминания заслуживает <strong>zlib-rs</strong> — проект, финансируемый ISRG (создатели Let’s Encrypt). В феврале 2025 года он обогнал zlib-ng: декомпрессия на 6–10 % быстрее, компрессия на 6 % быстрее на уровне по умолчанию. Чистый Rust, без unsafe в горячих путях, с автоматическим обнаружением SIMD.</p>
  <h3 id="zig-минимализм">Zig: минимализм</h3>
  <p id="v6Hu">В Zig 0.12 стандартная библиотека содержала <code>std.compress</code> с поддержкой deflate, gzip, zlib, xz и zstd. В Zig 0.15 компрессия была удалена (декомпрессия осталась) как часть масштабного пересмотра системы ввода-вывода. Сообщество создало библиотеку <code>comprezz</code>, извлечённую из стандартной библиотеки 0.14 и адаптированную под новые интерфейсы.</p>
  <p id="w9s0">Полные примеры: <a href="http://localhost:1313/blog/2026/compression/compress_demo.rs" target="_blank">compress_demo.rs</a>, <a href="http://localhost:1313/blog/2026/compression/compress_demo.go" target="_blank">compress_demo.go</a>, <a href="http://localhost:1313/blog/2026/compression/compress_demo.zig" target="_blank">compress_demo.zig</a>.</p>
  <h2 id="что-выбрать-на-практике">Что выбрать на практике</h2>
  <p id="cblJ">При выборе алгоритма сжатия ключевой вопрос — где узкое место. Если вы отдаёте статические ресурсы по HTTP (JavaScript-бандлы, CSS, шрифты), имеет смысл сжать их один раз Brotli на максимальном уровне и закешировать результат. Время компрессии здесь не критично — файл сжимается при деплое, а отдаётся тысячам клиентов.</p>
  <p id="xLF6">Для динамических ответов, которые генерируются на лету, расклад другой. Здесь важна скорость сжатия, и zstd на уровне 3–5 даёт лучший баланс: сжимает не хуже gzip-9, но в разы быстрее. gzip при этом остаётся разумным fallback — его понимают все клиенты, включая древние.</p>
  <p id="xeUT">Для архивов и бэкапов zstd заменяет gzip практически во всех сценариях. Если же критична скорость <em>распаковки</em> (игровые ресурсы, которые нужно загрузить за кадр, swap-раздел, горячий кеш), стоит посмотреть на LZ4 — он жертвует степенью сжатия, но декомпрессирует со скоростью более 4 ГБ/с.</p>
  <p id="4OAm">Отдельная ловушка — уровни сжатия. Между уровнями 1 и 6 разница в размере ощутима. Между 6 и 9 почти незаметна, а разница по времени — в разы. Уровень по умолчанию в большинстве реализаций выбран не случайно. Он уже находится в точке, где дальнейшее повышение уровня почти не уменьшает размер, но заметно замедляет процесс.</p>
  <p id="DGxl">И, наконец, не пытайтесь сжимать уже сжатое. JPEG, MP4, зашифрованные файлы, архивы внутри архивов — всё это данные с высокой энтропией, в которых закономерностей для компрессора не осталось. Попытка дожать их только добавит метаданные формата и увеличит размер.</p>
  <h2 id="заключение">Заключение</h2>
  <p id="Otlm">Сжатие данных — область, где теория и практика связаны напрямую. Шеннон определил предел в 1948 году. Хаффман приблизился к нему в 1952-м. Лемпель и Зив добавили словарный подход в 1977-м. DEFLATE объединил оба метода в 1990-х и держится стандартом до сих пор.</p>
  <p id="CzFX">Zstd — первая замена, набравшая критическую массу: от ядра Linux до браузеров и облачных хранилищ. Но фундаментальный результат Колмогорова остаётся: универсального компрессора не существует. Каждый алгоритм — компромисс между степенью сжатия, скоростью и объёмом памяти. Выбирайте тот, который соответствует вашей задаче.</p>
  <h2 id="источники">Источники</h2>
  <ul id="Gg6n">
    <li id="Mlcb"><a href="https://people.math.harvard.edu/~ctm/home/text/others/shannon/entropy/entropy.pdf" target="_blank">A Mathematical Theory of Communication (Shannon, 1948)</a> — статья Шеннона</li>
    <li id="Q0LY"><a href="https://ieeexplore.ieee.org/document/4051119" target="_blank">A Method for the Construction of Minimum-Redundancy Codes (Huffman, 1952)</a> — код Хаффмана</li>
    <li id="SPta"><a href="https://ieeexplore.ieee.org/document/1055714" target="_blank">A Universal Algorithm for Sequential Data Compression (Ziv &amp; Lempel, 1977)</a> — LZ77</li>
    <li id="KOWq"><a href="https://www.rfc-editor.org/rfc/rfc1951" target="_blank">RFC 1951: DEFLATE Compressed Data Format Specification</a> — спецификация DEFLATE</li>
    <li id="7EDz"><a href="https://www.bamsoftware.com/hacks/zipbomb/" target="_blank">A better zip bomb (David Fifield, 2019)</a> — нерекурсивная ZIP-бомба</li>
    <li id="IKB5"><a href="https://github.com/facebook/zstd" target="_blank">Zstandard (GitHub)</a> — исходный код zstd</li>
    <li id="KMIB"><a href="https://trifectatech.org/blog/zlib-rs-is-faster-than-c/" target="_blank">zlib-rs is faster than C (2025)</a> — результаты zlib-rs</li>
    <li id="6V5s"><a href="https://en.wikipedia.org/wiki/Kolmogorov_complexity" target="_blank">Kolmogorov Complexity (Wikipedia)</a> — алгоритмическая сложность</li>
    <li id="2WDj"><a href="https://paulcalvano.com/2024-03-19-choosing-between-gzip-brotli-and-zstandard-compression/" target="_blank">Choosing Between gzip, Brotli and zStandard (Paul Calvano, 2024)</a> — сравнение алгоритмов для веба</li>
  </ul>

]]></content:encoded></item></channel></rss>