{ "index": 30, "slug": "editorial-2027-03-practice-d-lessons", "title": "D для прикладной утилиты: как проверить, нужен ли новый язык", "excerpt": "Перед переходом на D измерьте workload, найдите границу с native-кодом и сравните стоимость toolchain с реальным выигрышем. Воспроизводимый benchmark и критерий готовности помогают принять решение, включая отказ от миграции.", "contentHtml": "
Утилита запускается медленно, занимает больше памяти, чем ожидалось, или требует вызова C-библиотеки. Команда сразу предлагает переписать её на D: язык компилируется в native binary, статически типизирован и умеет работать с C ABI. Но набор свойств ещё не объясняет симптом. Задержку может создавать сеть, формат файла, лишние копии или неверная граница API. Цена ошибочного перехода — новый компилятор, сборочный pipeline, обучение и месяцы поддержки без исправления узкого места.
\nРазберём лабораторный сценарий: CLI читает поток логов, считает строки и выделяет записи с ERROR. Для него задан бюджет — обработать один и тот же вход не дольше 20 секунд, а пиковая память должна остаться ниже 512 МБ. Числа и программа учебные; они не являются результатом замера чужой системы. Задача статьи — дать процедуру, по которой можно получить собственные данные и решить, оправдан ли прототип на D.
Фраза «нужна производительность» не задаёт инженерной задачи. Для CLI нужны время холодного старта, время обработки фиксированного входа, throughput (объём данных в секунду), пиковая память и размер артефакта. Для фонового процесса добавляются длительность работы, задержка отдельных операций и поведение после нескольких часов. Для вызова C-библиотеки важны типы, layout структуры, calling convention, ownership указателей и код ошибки.
\nЗапишите сценарий до эксперимента. Например: «утилита читает 2 ГБ логов через stdin, считает число строк и ошибок, должна завершиться менее чем за 20 секунд на Linux x86_64, результат — два числа в stdout». В описании есть единица нагрузки, граница времени, target и наблюдаемый output. Без этих условий benchmark превращается в сравнение разных программ на разных входах.
\nОфициальная спецификация описывает D как системный язык, который компилируется в native-код, статически типизирован и поддерживает автоматическое и ручное управление памятью. Это делает D разумным кандидатом для локальной CPU-нагрузки, самостоятельного бинарника или уже существующей C-интеграции. Но «кандидат» не означает «готовая замена»: библиотеки, toolchain, диагностика, сборка под архитектуры и сопровождение входят в стоимость решения.
\nУ D есть несколько механизмов, которые влияют на эксперимент. @nogc запрещает функциям прямо или косвенно выполнять операции с GC-кучей, но не превращает всю программу в код без аллокаций. @safe ограничивает часть операций, способных повредить память; @trusted оставляет ручную ответственность внутри маленькой проверенной границы. Контракты in и out выражают предусловия и постусловия, однако их запуск может зависеть от настроек компилятора. Поэтому каждый механизм нужно включить в проверку, а не использовать как рекламное обещание.
При C-вызове объявление extern(C) задаёт согласованную C-связь и последовательность вызова. Оно не угадывает неверный прототип, размер буфера, время жизни указателя или способ освобождения памяти. Даже если функция вызывается, это ещё не доказательство корректности всей границы.
| Симптом | Гипотеза | Проверка | Решение |
|---|---|---|---|
| Долгий запуск | Импорт, конфигурация или сеть, а не CPU | Профиль cold start с отключённой сетью и пустым рабочим набором | Исправить инициализацию; язык менять только при доказанном CPU-узком месте |
| Медленно читается файл | Алгоритм, декодирование или лишняя копия | Сравнить CPU и аллокации на одном входе | Сначала убрать копии; затем сделать парный прототип |
| Растёт RSS | Кэш, удерживаемые ссылки или буферизация всего файла | Замерить память по этапам и размеру входа | Перейти на потоковую обработку; не обещать эффект от native binary |
| Падает C-вызов | Прототип, layout, длина или ownership не совпадают | Сверить header, размеры, offsets, lifetime и код возврата | Изолировать FFI-wrapper и остановить вызов при неизвестном условии |
| Сложно доставлять | Несколько targets, зависимостей и ручных шагов | Собрать чистые артефакты и описать pipeline | Сопоставить стоимость toolchain с измеренным выигрышем |
Таблица задаёт порядок расследования, а не автоматический выбор. Если профиль показывает, что 80% времени занимает чтение сети, переход на D не меняет главную причину. Если потоковая обработка уже укладывается в бюджет, языковая миграция не нужна. Если bottleneck находится в вызове C, полезнее сначала проверить контракт границы, а не переписывать окружающий код.
\nСравнивайте два исполняемых файла, которым подаётся один байтовый вход и которые выдают один логический результат. Зафиксируйте checksum файла, версию исходников, компилятор, флаги, архитектуру, число повторов и способ измерения. Не смешивайте cold start с прогретым процессом. Не сравнивайте debug-сборку текущего инструмента с release-сборкой D.
\nДля учебного CLI создайте D-проект через DUB — официальный build и package manager экосистемы D — и замените содержимое source/app.d таким кодом:
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}\nПрограмма не хранит весь файл в памяти: она читает stdin построчно и печатает только итог. Это свойство нужно подтвердить измерением, а не выводить из синтаксиса. Создайте вход с известным размером и контрольной суммой, затем соберите release-вариант:
\ndub 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\nПоследняя команда использует GNU time и подходит для Linux. На macOS синтаксис системного time отличается, поэтому зафиксируйте другой измеритель, например hyperfine для времени и отдельный инструмент для памяти. Важно не название команды, а одинаковая методика для D и текущей реализации.
Один удачный прогон не доказывает превосходство. Выполните минимум пять повторов после одинаковой подготовки окружения и сохраните минимум, медиану и разброс. Повторите тест для маленького, среднего и предельного входа. Если результат меняется вместе с размером данных, запишите сложность и проверьте, не измеряется ли случайно файловый кеш.
\nПроверяйте корректность раньше скорости. Для каждого входа сравните stdout, код возврата и поведение на повреждённой строке. Затем сравните время, peak RSS, размер бинарника и время сборки с чистого checkout. Если D быстрее, но выдаёт другое число ошибок или не умеет объяснить невалидный UTF-8, это не выигрыш, а несовместимый результат.
\n| Измерение | Что фиксировать | Критерий принятия |
|---|---|---|
| Корректность | stdout, stderr, exit code на тех же входах | Результаты совпадают или различие явно согласовано |
| Runtime | медиана и диапазон пяти повторов | Медиана ниже бюджета и выигрыш не исчезает на предельном входе |
| Память | peak RSS и зависимость от размера файла | Пик не превышает лимит; рост объясним выбранным алгоритмом |
| Доставка | время clean build, размер, targets, зависимости | Команда может повторить сборку без ручного локального состояния |
| Поддержка | сложность отладки, тестов, обновлений и FFI | Есть владелец и понятная процедура изменения |
Критерий должен быть задан до просмотра результатов. Например: «D принимаем в следующий этап, если на трёх размерах входа сохраняется корректность, медиана быстрее текущей реализации минимум на 25%, peak RSS не выходит за лимит, а clean build и cross-compilation укладываются в согласованный pipeline». Порог 25% — проектное решение, а не свойство D. Если хотя бы одно обязательное условие не выполнено, прототип возвращается на разбор или закрывается.
\nNative-интеграция может быть главным аргументом в пользу D, но она же добавляет риск. Указатель и длина образуют пару: адрес сам по себе не сообщает, сколько байт разрешено читать. До вызова wrapper должен проверить диапазон, нулевой адрес, alignment и время жизни буфера. Если C сохраняет адрес после возврата, временный slice нельзя считать достаточным контрактом.
\nСтруктуру нельзя объявлять совместимой по совпадению имён полей. Сверьте размер, offsets, alignment, порядок байтов и calling convention с теми header и compiler flags, которыми собрана библиотека. Если API возвращает указатель, назовите allocator и парную функцию освобождения. Вызов общего free не становится правильным только потому, что он компилируется.
В D выделите короткую границу: она проверяет вход, вызывает C, сначала смотрит return code и только затем читает output. Ошибка должна превращаться в результат, который верхний слой умеет обработать. @trusted полезен как табличка ответственности, но не заменяет проверку ABI и документации библиотеки.
Учебный CLI не моделирует сервис с конкурентными запросами, задержкой сети, большим числом файлов, интерактивным UX или длительным жизненным циклом процесса. Результат на Linux x86_64 нельзя переносить на arm64, Windows, другой компилятор, другую версию runtime или другой размер входа без повторной проверки.
\nПороги 20 секунд, 512 МБ и 25% вымышлены для формы эксперимента. Они не обещают, что D будет быстрее или экономнее. @nogc контролирует GC-аллокации конкретной функции, но не отменяет системный allocator, I/O и сторонние библиотеки. @safe не делает C-код безопасным, а extern(C) не проверяет ownership. Контракты не заменяют тесты на повреждённых входах.
Остановите переход, если workload не воспроизводится, критерий успеха появился после замеров, результат отличается по смыслу, C-граница не описана или clean build требует ручного состояния. Отказ от нового языка — не провал эксперимента. Это экономически полезный результат, если он избавляет команду от миграции, которая не исправляет исходную причину.
\nРешение готово к следующему этапу, когда другой инженер получает фиксированный вход, команды сборки, версии toolchain и скрипт измерения; может повторить проверку на текущей и D-реализации; видит одинаковую корректность, время, память и стоимость доставки; понимает границы ABI и знает стоп-условия. Если этих данных нет, готов только вопрос, а не обоснование перехода.
\n@nogc и атрибуты безопасности; спецификация отдельно оговаривает, что выполнение контрактов зависит от реализации.@safe, @trusted и @system, включая ограничения trusted-кода.extern(C), совместимость типов и ограничения ручной FFI-границы.