{ "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 — это граница вопроса, а не причина

\n

Слово «runtime» скрывает несколько разных механизмов. D-код может создать временный массив. Сборщик может увидеть доступные объекты. Foreign function может выделить память по собственному контракту. I/O может заблокировать поток. Эти события находятся рядом в трассе запроса, но не имеют одной проверки.

\n

Узкий вопрос выглядит так: «При одинаковых входе и ответе создаёт ли этот этап больше временного состояния, чем выбранный бюджет?» Такой вопрос допускает проверку. Вопрос «D тормозит?» не говорит, что измерять и какое действие будет правильным.

\n

Механизм одного request path

\n

Разделите запрос на четыре этапа: decode, validate, compute и encode. Для каждого запишите вход и выход. Рядом отметьте границы, которые не принадлежат учебному примеру: вызов C-библиотеки, сеть, файл или база данных. Граница должна остаться в trace даже тогда, когда вызов ещё не выполняется.

\n

В учебной модели 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 с явным статусомНеисполненный внешний вызов не принимают за измеренный
\n

Вариант allocation-heavy может получить 12 units, а вариант reuse — 3. Если оба возвращают тот же JSON и выполняют то же число work units, модель выделяет одну гипотезу: промежуточное состояние. Она не доказывает, что второй вариант быстрее на production-трафике.

\n

Пример: сохраняем ответ и меняем только промежуточное состояние

\n

Ниже показан маленький пример на D. Он ограничен вычислением суммы. В нём нет HTTP, сериализации, GC-профайлера и FFI. Комментарии помечают места, где реальный сервис должен добавить отдельную проверку.

\n
struct 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.

\n

Если в реальном коде encode создаёт строку, это нужно увидеть в trace и подтвердить профилем. Нельзя вывести размер выделения из одного только названия функции. Нельзя считать вызов C безопасным по факту успешной компиляции: проверьте calling convention, layout, ownership, освобождение памяти и возможность блокировки.

\n
\"Схема
Маршрут одного запроса. Граница GC на схеме означает точку проверки, а не подтверждённую паузу. FFI и I/O отмечены как внешние границы, пока вызов не измерен.
\n

Симптом → причина → проверка → действие

\n
СимптомПричина-гипотезаПроверкаДействие
Растёт 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 с реальным профилемСохранить модель как гипотезу, а ответ получить инструментом на нужной среде
\n

Порядок проверки

\n
  1. Зафиксируйте симптом. Назовите endpoint, вход, status, body и условие, при котором проявляется проблема. Не начинайте с ярлыка «runtime D».
  2. Сузьте границу. Выберите один участок: временные объекты, GC, FFI или I/O. Остальные границы запишите как неизвестные или неисполненные.
  3. Сохраните контракт. Сравните успешный и ошибочный путь. Для успешного пути зафиксируйте body, для ошибочного — status и форму ошибки.
  4. Соберите базовый trace. Запишите вход, work units, allocation units, выбранный budget и версии compiler, DRuntime, ОС и библиотеки.
  5. Проверьте альтернативу. Измените только промежуточное состояние. Если меняются body или work units, сравнение allocation преждевременно.
  6. Измерьте реальную среду. Запустите подходящий profiler или benchmark на том же endpoint. Учебные units не подменяют байты, CPU time, pauses и latency.
  7. Проверьте отрицательный путь. Повторите тест с невалидным input, ошибкой FFI и отказом I/O, если эти границы входят в endpoint.
  8. Сформулируйте действие. Меняйте локальный код только при подтверждённой гипотезе. После изменения повторите базовый и отрицательный сценарии.
\n

Ограничения

\n

Такой разбор не моделирует поведение всей системы. Он не описывает распределение памяти, алгоритм сборщика, scheduler, конкуренцию потоков, backpressure, сетевые повторы, сериализацию настоящего протокола, лимиты контейнера или нагрузку пользователей. Он не подтверждает production-результат и не заменяет нагрузочный тест.

\n

@nogc ограничивает D-код, но не отменяет правила внешнего ABI и не запрещает другой части процесса запускать сборку. FFI boundary требует отдельного договора о типах и владении памятью. I/O boundary требует отдельного измерения ожидания и поведения при обрыве. Если эти условия неизвестны, честное действие — оставить их неизвестными и назначить проверку, а не заполнить пробел предположением.

\n

Критерий готовности

\n

Проверка закрыта, когда для одного входа сохранены базовый trace и trace после изменения; успешный ответ совпадает; error path проверен; выбранная причина подтверждена подходящим измерением; версии и условия запуска записаны; FFI и I/O имеют явный статус; а действие не опирается на учебные allocation units как на production-метрику. Если хотя бы одно условие не выполнено, результат — новая гипотеза, а не доказанное исправление.

\n

Проверяемые источники

\n" }