{ "index": 225, "slug": "editorial-2021-10-practice-d-runtime-service", "title": "D-runtime в сервисе: как найти лишнее выделение на одном запросе", "excerpt": "Если endpoint обвиняют в медленном D-runtime, начните с одного запроса: зафиксируйте результат, отделите GC от FFI и I/O, а затем проверьте гипотезу измерением, а не настройкой вслепую.", "contentHtml": "
Endpoint иногда отвечает заметно дольше обычного, а в профиле рядом с ним появляется GC. Команда называет причиной «медленный D-runtime» и меняет глобальные настройки сборщика. Симптом может исчезнуть на коротком тесте, но контракт запроса и границы внешних вызовов остаются непроверенными. Цена ошибки — лишний риск в каждом запросе: можно получить больше памяти, длиннее паузы или несовместимость с библиотекой, не доказав, что исправлен нужный участок.
\nНадёжнее начать с одного request path. Зафиксируйте вход, успешный ответ и ошибочный путь. Затем отделите временные объекты от GC boundary, FFI и I/O. Сначала нужно доказать, что два варианта делают одну работу и возвращают один результат. Только после этого имеет смысл обсуждать allocation, настройки DRuntime или переписывание горячего участка.
\nСлово «runtime» скрывает несколько разных механизмов. D-код может создать временный массив. Сборщик может увидеть доступные объекты. Foreign function может выделить память по собственному контракту. I/O может заблокировать поток. Эти события находятся рядом в трассе запроса, но не имеют одной проверки.
\nУзкий вопрос выглядит так: «При одинаковых входе и ответе создаёт ли этот этап больше временного состояния, чем выбранный бюджет?» Такой вопрос допускает проверку. Вопрос «D тормозит?» не говорит, что измерять и какое действие будет правильным.
\nРазделите запрос на четыре этапа: decode, validate, compute и encode. Для каждого запишите вход и выход. Рядом отметьте границы, которые не принадлежат учебному примеру: вызов C-библиотеки, сеть, файл или база данных. Граница должна остаться в trace даже тогда, когда вызов ещё не выполняется.
В учебной модели allocation units — условные единицы счёта. Они не равны байтам, времени CPU, latency или числу запусков GC. Бюджет 6 означает только одно: выбранный вариант превышает порог, заданный примером. Реальный порог нужно выбрать для конкретного endpoint и подтвердить инструментом в конкретной версии компилятора, DRuntime и окружения.
\n| Поле | Что фиксируем | Что проверяем |
|---|---|---|
| Вход | sum(19, 23), request id | Оба варианта получают одинаковые данные |
| Ответ | {"requestId":"runtime-d-42","total":42} | Оптимизация не меняет контракт |
| Работа | Одинаковое число work units | Сравнение не прячет другую алгоритмическую работу |
| Allocation | Условные units и budget | Видно превышение выбранного порога |
| Границы | GC, FFI, I/O с явным статусом | Неисполненный внешний вызов не принимают за измеренный |
Вариант allocation-heavy может получить 12 units, а вариант reuse — 3. Если оба возвращают тот же JSON и выполняют то же число work units, модель выделяет одну гипотезу: промежуточное состояние. Она не доказывает, что второй вариант быстрее на production-трафике.
Ниже показан маленький пример на D. Он ограничен вычислением суммы. В нём нет HTTP, сериализации, GC-профайлера и FFI. Комментарии помечают места, где реальный сервис должен добавить отдельную проверку.
\nstruct Response {\n string requestId;\n int total;\n}\n\nResponse handle(int left, int right) {\n // Учебный контракт: результат зависит только от входа.\n return Response(\"runtime-d-42\", left + right);\n}\n\n@nogc int compute(int left, int right) {\n // Здесь нет операций, которые требуют GC-аллокации.\n return left + right;\n}\n\nvoid requestTrace() {\n // decode -> validate -> compute -> encode\n // FFI: not performed; I/O: not performed.\n auto response = handle(19, 23);\n assert(response.total == 42);\n}\nАннотация @nogc полезна как ограничение на вызываемый D-код: компилятор проверяет конструкции и вызовы, которые могут потребовать GC-аллокации. Но она не делает безопасной неизвестную внешнюю функцию. Она также не доказывает отсутствие пауз сборщика во всём процессе. Другой поток может работать с GC, а FFI-библиотека может иметь собственный allocator. Поэтому атрибут — часть границы, а не итоговый performance report.
Если в реальном коде encode создаёт строку, это нужно увидеть в trace и подтвердить профилем. Нельзя вывести размер выделения из одного только названия функции. Нельзя считать вызов C безопасным по факту успешной компиляции: проверьте calling convention, layout, ownership, освобождение памяти и возможность блокировки.
| Симптом | Причина-гипотеза | Проверка | Действие |
|---|---|---|---|
| Растёт allocation на успешном запросе | Временное состояние создаётся на decode или encode | Сравнить одинаковый input, output, work units и allocation trace | Убрать промежуточную копию или переиспользовать буфер; повторить проверку контракта |
| В трассе есть GC boundary | Выбранный путь пересёк условный порог | Проверить реальный профиль с версией compiler, DRuntime и платформой | Изменять локальный участок только после измерения; не менять глобальную настройку вслепую |
| После FFI меняется задержка | Библиотека выделяет память или блокирует поток | Проверить ABI, ownership, allocator и время вызова отдельно | Согласовать контракт с владельцем библиотеки; не приписывать эффект GC |
| Endpoint медленный только при ошибке | В error path остаётся дополнительная сериализация или retry | Прогнать невалидный input и сравнить trace с успешным путём | Исправить error path и добавить отдельный критерий для него |
| Учебный тест зелёный, сервис всё ещё медленный | Модель не содержит HTTP, concurrency, backpressure или I/O | Воспроизвести один живой endpoint с реальным профилем | Сохранить модель как гипотезу, а ответ получить инструментом на нужной среде |
Такой разбор не моделирует поведение всей системы. Он не описывает распределение памяти, алгоритм сборщика, scheduler, конкуренцию потоков, backpressure, сетевые повторы, сериализацию настоящего протокола, лимиты контейнера или нагрузку пользователей. Он не подтверждает production-результат и не заменяет нагрузочный тест.
\n@nogc ограничивает D-код, но не отменяет правила внешнего ABI и не запрещает другой части процесса запускать сборку. FFI boundary требует отдельного договора о типах и владении памятью. I/O boundary требует отдельного измерения ожидания и поведения при обрыве. Если эти условия неизвестны, честное действие — оставить их неизвестными и назначить проверку, а не заполнить пробел предположением.
Проверка закрыта, когда для одного входа сохранены базовый trace и trace после изменения; успешный ответ совпадает; error path проверен; выбранная причина подтверждена подходящим измерением; версии и условия запуска записаны; FFI и I/O имеют явный статус; а действие не опирается на учебные allocation units как на production-метрику. Если хотя бы одно условие не выполнено, результат — новая гипотеза, а не доказанное исправление.
\n@nogc.