{ "index": 30, "slug": "editorial-2027-03-practice-d-lessons", "title": "D для прикладной утилиты: как проверить, нужен ли новый язык", "excerpt": "Перед переходом на D измерьте workload, найдите границу с native-кодом и сравните стоимость toolchain с реальным выигрышем. Учебный фильтр и критерий готовности помогают принять решение, включая отказ от миграции.", "contentHtml": "
Утилита запускается медленно, занимает больше памяти, чем ожидалось, или требует вызова C-библиотеки. Команда сразу предлагает переписать её на D: язык компилируется в native binary, умеет работать с C ABI и даёт контроль над памятью. Но симптом ещё не показывает причину. Задержку может создавать сеть, формат файла, лишние копии или неверная граница API. Цена ошибочного выбора — новый компилятор, сборочный pipeline, обучение и месяцы поддержки без исправления узкого места.
\nТезис простой: D стоит проверять не по списку свойств языка, а по контракту задачи. Сначала зафиксируйте workload и бюджет, затем найдите участок, который действительно изменится при смене языка. После этого сравните D с текущим инструментом по измеримому эффекту и полной стоимости доставки. Если доказательства не складываются, решение остаться на текущем языке будет корректным результатом.
\nФраза «нужна производительность» не задаёт задачи. Для CLI важны время запуска, время обработки одного входа, пиковая память и размер бинарника. Для фонового процесса важны throughput, steady-state latency и поведение после нескольких часов работы. Для сервиса добавляются конкуренция, timeout и наблюдаемость. Для вызова C-библиотеки важны layout структуры, calling convention, ownership указателей и код ошибки.
\nЗапишите один сценарий, а не среднее впечатление. Например: «утилита читает 2 ГБ логов, должна обработать файл менее чем за 20 секунд, запускается на Linux x86_64 и arm64, а парсер отдаёт данные в C-библиотеку». В таком описании уже видны единица нагрузки, предел времени, targets и native boundary. Без них benchmark легко превращается в сравнение несопоставимых программ.
\nУ решения есть четыре связанные части. Workload показывает, сколько данных и операций проходит через код. Бюджет latency задаёт допустимую цену одной операции. Native boundary показывает, нужен ли прямой доступ к C, системному вызову или нативному формату. Target matrix показывает, сколько раз придётся собрать, протестировать и доставить бинарник.
\nD может быть сильным кандидатом, когда горячий участок вычисляет данные локально, нужен native deployment или уже есть C ABI. Но это только основание для эксперимента. У D остаются стоимость компилятора и зависимостей, различия runtime, диагностика бинарника, упаковка под несколько архитектур и время команды. Нативный бинарник не отменяет сетевую задержку и не делает внешний API безопасным.
\nКонтрактные проверки полезны внутри функции. Precondition проверяет входной инвариант, postcondition — свойство результата. Они не заменяют проверку пользовательского файла, обработку ошибки и тесты. Проверка должна принадлежать тому уровню, который владеет условием: parser проверяет формат, доменный код — смысл, wrapper — указатель, длину и время жизни.
\n| Симптом | Возможная причина | Проверка | Действие |
|---|---|---|---|
| Долгий запуск | Импорт модулей, чтение конфигурации, сеть | Профиль cold start с отключённой сетью | Исправить инициализацию; язык менять только при доказанном CPU-узком месте |
| Медленная обработка файла | Копии строк, декодирование, неверный алгоритм | Профиль CPU и аллокаций на одном входе | Сравнить алгоритм и парный прототип D |
| Рост RSS | Долгоживущие ссылки, кэш, фрагментация | Снять профиль памяти по этапам batch | Укоротить lifetime; не отключать GC по одному графику |
| Падение в C-вызове | Неверная длина, layout или ownership | Сверить header, размер, offset и код возврата | Изолировать wrapper и остановить вызов при несовпадении |
| Сложная доставка | Несколько архитектур и ручная упаковка | Собрать чистые артефакты для каждого target | Сравнить цену toolchain с выигрышем runtime |
Следующая функция не измеряет скорость и не выбирает язык автоматически. Она превращает карточку задачи в явные условия. Числа учебные: throughput 12 000 и бюджет 20 мс нельзя переносить на другое железо. В реальном решении их заменяют измерениями одного workload.
\nfunction decideD(input) {\n const throughput = Number(input.throughput);\n const latencyBudgetMs = Number(input.latencyBudgetMs);\n const targets = Number(input.deploymentTargets);\n if (!Number.isFinite(throughput) || throughput <= 0) return { decision: 'reject', reason: 'нет измеримой нагрузки' };\n if (latencyBudgetMs <= 0) return { decision: 'reject', reason: 'нет бюджета задержки' };\n if (targets > 2 && !input.nativeBoundary) return { decision: 'compare', reason: 'сначала сравнить toolchain' };\n if (input.nativeBoundary && throughput > 10000) return { decision: 'prototype-d', reason: 'есть основание для парного прототипа' };\n return { decision: 'keep-current-tool', reason: 'смена языка не обоснована' };\n}\n\nconsole.log(decideD({ throughput: 12000, latencyBudgetMs: 20, nativeBoundary: true, deploymentTargets: 1 }));\n// Учебный результат: { decision: 'prototype-d', ... }\nПоложительная ветка означает только «собрать прототип». Она не означает «переписать продукт». Ветка keep-current-tool нужна намеренно: если нагрузка мала, границы с native-кодом нет, а текущий стек уже покрывает доставку, новый язык увеличит риск без доказанной пользы. Ветка compare останавливает преждевременный выбор при широкой матрице targets.
Указатель и длина образуют один контракт. Сам указатель не сообщает, сколько байт можно читать. Wrapper должен получить буфер, проверить его владельца и диапазон, а затем передать в C только проверенный slice или пару pointer/length. Если библиотека сохраняет адрес после возврата, обычного временного буфера недостаточно: нужен согласованный lifetime или копия.
\nСтруктуру тоже нельзя считать совместимой по имени полей. Сверьте размер, offsets, alignment, порядок байтов и calling convention. Отдельно зафиксируйте значения кода ошибки. Частично заполненный output не равен успешному результату. Сначала проверьте код возврата, затем версию и layout, потом отдайте значение доменному коду.
\nФильтр не заменяет profiler, benchmark и review ABI. Его пороги вымышлены и нужны только для формы проверки. Даже хороший benchmark не переносит результат на другую архитектуру, версию компилятора, размер данных или режим нагрузки. Нативная сборка не гарантирует меньшую память. Контракт не исправляет неверную бизнес-логику.
\nОценка должна учитывать отрицательный путь. Если wrapper не может доказать длину, lifetime или layout, вызов нужно остановить. Если D выигрывает только в искусственном микротесте, но требует отдельной упаковки и дежурства, выигрыш не доказан. Если текущий язык после устранения лишних копий укладывается в бюджет, миграция не нужна.
\nРешение готово, когда для одного и того же workload есть повторяемые замеры текущего инструмента и прототипа D, описаны targets и native boundary, а также измерена цена сборки и поддержки. Для каждого результата указаны вход, версия toolchain, архитектура, число повторов и критерий успеха. Вызов C проходит только после проверки размера, lifetime и кода ошибки. Команда может объяснить не только почему D быстрее, но и почему это преимущество покрывает стоимость доставки.
\n@safe, @trusted и @system.