8 lines
23 KiB
JSON
8 lines
23 KiB
JSON
{
|
||
"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\") >= 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 < ../benchmark/input/logs.txt > /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>"
|
||
}
|