Files
progcode/editorial/agent-rewrites/030.json
T

8 lines
23 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 30,
"slug": "editorial-2027-03-practice-d-lessons",
"title": "D для прикладной утилиты: как проверить, нужен ли новый язык",
"excerpt": "Перед переходом на D измерьте workload, найдите границу с native-кодом и сравните стоимость toolchain с реальным выигрышем. Воспроизводимый benchmark и критерий готовности помогают принять решение, включая отказ от миграции.",
"contentHtml": "<p>Утилита запускается медленно, занимает больше памяти, чем ожидалось, или требует вызова C-библиотеки. Команда сразу предлагает переписать её на D: язык компилируется в native binary, статически типизирован и умеет работать с C ABI. Но набор свойств ещё не объясняет симптом. Задержку может создавать сеть, формат файла, лишние копии или неверная граница API. Цена ошибочного перехода — новый компилятор, сборочный pipeline, обучение и месяцы поддержки без исправления узкого места.</p>\n<p>Разберём лабораторный сценарий: CLI читает поток логов, считает строки и выделяет записи с <code>ERROR</code>. Для него задан бюджет — обработать один и тот же вход не дольше 20 секунд, а пиковая память должна остаться ниже 512 МБ. Числа и программа учебные; они не являются результатом замера чужой системы. Задача статьи — дать процедуру, по которой можно получить собственные данные и решить, оправдан ли прототип на D.</p>\n<h2>Сначала отделите симптом от причины</h2>\n<p>Фраза «нужна производительность» не задаёт инженерной задачи. Для CLI нужны время холодного старта, время обработки фиксированного входа, throughput (объём данных в секунду), пиковая память и размер артефакта. Для фонового процесса добавляются длительность работы, задержка отдельных операций и поведение после нескольких часов. Для вызова C-библиотеки важны типы, layout структуры, calling convention, ownership указателей и код ошибки.</p>\n<p>Запишите сценарий до эксперимента. Например: «утилита читает 2 ГБ логов через stdin, считает число строк и ошибок, должна завершиться менее чем за 20 секунд на Linux x86_64, результат — два числа в stdout». В описании есть единица нагрузки, граница времени, target и наблюдаемый output. Без этих условий benchmark превращается в сравнение разных программ на разных входах.</p>\n<figure><img src=\"/assets/editorial/2027/d-lessons-2027-runtime-tradeoff-map.svg\" alt=\"Карта выбора языка для прикладной утилиты: workload и ограничения ведут к парному сравнению D с текущим инструментом, затем к прототипу или остановке\" loading=\"lazy\" /><figcaption>Смена языка появляется только после фиксации нагрузки, ограничений и одинаковых условий сравнения. Отсутствие измеримого узкого места — достаточная причина остановиться.</figcaption></figure>\n<h2>Что именно добавляет D</h2>\n<p>Официальная спецификация описывает D как системный язык, который компилируется в native-код, статически типизирован и поддерживает автоматическое и ручное управление памятью. Это делает D разумным кандидатом для локальной CPU-нагрузки, самостоятельного бинарника или уже существующей C-интеграции. Но «кандидат» не означает «готовая замена»: библиотеки, toolchain, диагностика, сборка под архитектуры и сопровождение входят в стоимость решения.</p>\n<p>У D есть несколько механизмов, которые влияют на эксперимент. <code>@nogc</code> запрещает функциям прямо или косвенно выполнять операции с GC-кучей, но не превращает всю программу в код без аллокаций. <code>@safe</code> ограничивает часть операций, способных повредить память; <code>@trusted</code> оставляет ручную ответственность внутри маленькой проверенной границы. Контракты <code>in</code> и <code>out</code> выражают предусловия и постусловия, однако их запуск может зависеть от настроек компилятора. Поэтому каждый механизм нужно включить в проверку, а не использовать как рекламное обещание.</p>\n<p>При C-вызове объявление <code>extern(C)</code> задаёт согласованную C-связь и последовательность вызова. Оно не угадывает неверный прототип, размер буфера, время жизни указателя или способ освобождения памяти. Даже если функция вызывается, это ещё не доказательство корректности всей границы.</p>\n<h2>Матрица решения до прототипа</h2>\n<div class=\"table-scroll\"><table><caption>Симптом → гипотеза → проверка → решение</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Гипотеза</th><th scope=\"col\">Проверка</th><th scope=\"col\">Решение</th></tr></thead><tbody><tr><td>Долгий запуск</td><td>Импорт, конфигурация или сеть, а не CPU</td><td>Профиль cold start с отключённой сетью и пустым рабочим набором</td><td>Исправить инициализацию; язык менять только при доказанном CPU-узком месте</td></tr><tr><td>Медленно читается файл</td><td>Алгоритм, декодирование или лишняя копия</td><td>Сравнить CPU и аллокации на одном входе</td><td>Сначала убрать копии; затем сделать парный прототип</td></tr><tr><td>Растёт RSS</td><td>Кэш, удерживаемые ссылки или буферизация всего файла</td><td>Замерить память по этапам и размеру входа</td><td>Перейти на потоковую обработку; не обещать эффект от native binary</td></tr><tr><td>Падает C-вызов</td><td>Прототип, layout, длина или ownership не совпадают</td><td>Сверить header, размеры, offsets, lifetime и код возврата</td><td>Изолировать FFI-wrapper и остановить вызов при неизвестном условии</td></tr><tr><td>Сложно доставлять</td><td>Несколько targets, зависимостей и ручных шагов</td><td>Собрать чистые артефакты и описать pipeline</td><td>Сопоставить стоимость toolchain с измеренным выигрышем</td></tr></tbody></table></div>\n<p>Таблица задаёт порядок расследования, а не автоматический выбор. Если профиль показывает, что 80% времени занимает чтение сети, переход на D не меняет главную причину. Если потоковая обработка уже укладывается в бюджет, языковая миграция не нужна. Если bottleneck находится в вызове C, полезнее сначала проверить контракт границы, а не переписывать окружающий код.</p>\n<h2>Сделайте benchmark воспроизводимым</h2>\n<p>Сравнивайте два исполняемых файла, которым подаётся один байтовый вход и которые выдают один логический результат. Зафиксируйте checksum файла, версию исходников, компилятор, флаги, архитектуру, число повторов и способ измерения. Не смешивайте cold start с прогретым процессом. Не сравнивайте debug-сборку текущего инструмента с release-сборкой D.</p>\n<p>Для учебного CLI создайте D-проект через DUB — официальный build и package manager экосистемы D — и замените содержимое <code>source/app.d</code> таким кодом:</p>\n<pre><code>import std.stdio : stdin, writefln;\nimport std.string : indexOf;\n\nvoid main()\n{\n size_t lines;\n size_t errors;\n\n foreach (line; stdin.byLine())\n {\n ++lines;\n if (line.indexOf(\"ERROR\") &gt;= 0)\n ++errors;\n }\n\n writefln(\"%s %s\", lines, errors);\n}</code></pre>\n<p>Программа не хранит весь файл в памяти: она читает stdin построчно и печатает только итог. Это свойство нужно подтвердить измерением, а не выводить из синтаксиса. Создайте вход с известным размером и контрольной суммой, затем соберите release-вариант:</p>\n<pre><code>dub init log-counter --type=application\ncd log-counter\ndub test\ndub build --build=release\nsha256sum ../benchmark/input/logs.txt\n/usr/bin/time -f '%e sec %M KB' ./log-counter &lt; ../benchmark/input/logs.txt &gt; /dev/null</code></pre>\n<p>Последняя команда использует GNU <code>time</code> и подходит для Linux. На macOS синтаксис системного <code>time</code> отличается, поэтому зафиксируйте другой измеритель, например <code>hyperfine</code> для времени и отдельный инструмент для памяти. Важно не название команды, а одинаковая методика для D и текущей реализации.</p>\n<h2>Сравните не только секунды</h2>\n<p>Один удачный прогон не доказывает превосходство. Выполните минимум пять повторов после одинаковой подготовки окружения и сохраните минимум, медиану и разброс. Повторите тест для маленького, среднего и предельного входа. Если результат меняется вместе с размером данных, запишите сложность и проверьте, не измеряется ли случайно файловый кеш.</p>\n<p>Проверяйте корректность раньше скорости. Для каждого входа сравните stdout, код возврата и поведение на повреждённой строке. Затем сравните время, peak RSS, размер бинарника и время сборки с чистого checkout. Если D быстрее, но выдаёт другое число ошибок или не умеет объяснить невалидный UTF-8, это не выигрыш, а несовместимый результат.</p>\n<table><caption>Минимальный протокол сравнения</caption><thead><tr><th scope=\"col\">Измерение</th><th scope=\"col\">Что фиксировать</th><th scope=\"col\">Критерий принятия</th></tr></thead><tbody><tr><td>Корректность</td><td>stdout, stderr, exit code на тех же входах</td><td>Результаты совпадают или различие явно согласовано</td></tr><tr><td>Runtime</td><td>медиана и диапазон пяти повторов</td><td>Медиана ниже бюджета и выигрыш не исчезает на предельном входе</td></tr><tr><td>Память</td><td>peak RSS и зависимость от размера файла</td><td>Пик не превышает лимит; рост объясним выбранным алгоритмом</td></tr><tr><td>Доставка</td><td>время clean build, размер, targets, зависимости</td><td>Команда может повторить сборку без ручного локального состояния</td></tr><tr><td>Поддержка</td><td>сложность отладки, тестов, обновлений и FFI</td><td>Есть владелец и понятная процедура изменения</td></tr></tbody></table>\n<p>Критерий должен быть задан до просмотра результатов. Например: «D принимаем в следующий этап, если на трёх размерах входа сохраняется корректность, медиана быстрее текущей реализации минимум на 25%, peak RSS не выходит за лимит, а clean build и cross-compilation укладываются в согласованный pipeline». Порог 25% — проектное решение, а не свойство D. Если хотя бы одно обязательное условие не выполнено, прототип возвращается на разбор или закрывается.</p>\n<h2>Как проверить границу с C</h2>\n<p>Native-интеграция может быть главным аргументом в пользу D, но она же добавляет риск. Указатель и длина образуют пару: адрес сам по себе не сообщает, сколько байт разрешено читать. До вызова wrapper должен проверить диапазон, нулевой адрес, alignment и время жизни буфера. Если C сохраняет адрес после возврата, временный slice нельзя считать достаточным контрактом.</p>\n<p>Структуру нельзя объявлять совместимой по совпадению имён полей. Сверьте размер, offsets, alignment, порядок байтов и calling convention с теми header и compiler flags, которыми собрана библиотека. Если API возвращает указатель, назовите allocator и парную функцию освобождения. Вызов общего <code>free</code> не становится правильным только потому, что он компилируется.</p>\n<p>В D выделите короткую границу: она проверяет вход, вызывает C, сначала смотрит return code и только затем читает output. Ошибка должна превращаться в результат, который верхний слой умеет обработать. <code>@trusted</code> полезен как табличка ответственности, но не заменяет проверку ABI и документации библиотеки.</p>\n<h2>Действия по порядку</h2>\n<ol><li>Записать один workload: вход, output, бюджет времени, лимит памяти, targets и допустимые ошибки.</li><li>Повторить симптом на фиксированном входе и измерить текущую реализацию. Сначала найти CPU, I/O, память или границу API.</li><li>Зафиксировать одинаковую методику: checksum, release-флаги, компилятор, архитектура, прогрев и число повторов.</li><li>Собрать маленький D-прототип без лишних библиотек и сравнить его корректность с текущим инструментом.</li><li>Измерить медиану, разброс, peak RSS, clean build, размер бинарника и цену сборки под каждый target.</li><li>Если есть C, проверить прототип, layout, указатели, lifetime, allocator и код ошибки до расширения wrapper.</li><li>Сопоставить измеренный выигрыш с ежемесячной стоимостью поддержки: обновлениями, отладкой, наймом и доставкой.</li><li>Принять решение по заранее заданному критерию: мигрировать, оставить узкий компонент на D или остаться на текущем языке.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Учебный CLI не моделирует сервис с конкурентными запросами, задержкой сети, большим числом файлов, интерактивным UX или длительным жизненным циклом процесса. Результат на Linux x86_64 нельзя переносить на arm64, Windows, другой компилятор, другую версию runtime или другой размер входа без повторной проверки.</p>\n<p>Пороги 20 секунд, 512 МБ и 25% вымышлены для формы эксперимента. Они не обещают, что D будет быстрее или экономнее. <code>@nogc</code> контролирует GC-аллокации конкретной функции, но не отменяет системный allocator, I/O и сторонние библиотеки. <code>@safe</code> не делает C-код безопасным, а <code>extern(C)</code> не проверяет ownership. Контракты не заменяют тесты на повреждённых входах.</p>\n<p>Остановите переход, если workload не воспроизводится, критерий успеха появился после замеров, результат отличается по смыслу, C-граница не описана или clean build требует ручного состояния. Отказ от нового языка — не провал эксперимента. Это экономически полезный результат, если он избавляет команду от миграции, которая не исправляет исходную причину.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Решение готово к следующему этапу, когда другой инженер получает фиксированный вход, команды сборки, версии toolchain и скрипт измерения; может повторить проверку на текущей и D-реализации; видит одинаковую корректность, время, память и стоимость доставки; понимает границы ABI и знает стоп-условия. Если этих данных нет, готов только вопрос, а не обоснование перехода.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://dlang.org/spec/intro.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Introduction</a> — native-компиляция, статическая типизация и модели управления памятью.</li><li><a href=\"https://dlang.org/spec/function.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Functions</a> — контракты функций, <code>@nogc</code> и атрибуты безопасности; спецификация отдельно оговаривает, что выполнение контрактов зависит от реализации.</li><li><a href=\"https://dlang.org/spec/memory-safe-d.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Memory Safety</a> — различия <code>@safe</code>, <code>@trusted</code> и <code>@system</code>, включая ограничения trusted-кода.</li><li><a href=\"https://dlang.org/spec/interfaceToC.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Interfacing to C</a> — вызов C, <code>extern(C)</code>, совместимость типов и ограничения ручной FFI-границы.</li><li><a href=\"https://dlang.org/install.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Programming Language: Install</a> — официальный способ установить компилятор и DUB для воспроизводимого эксперимента.</li></ul>"
}