Когда один handler называют «быстрым на D», обычно пропускают механизм. Проблема в том, что в ответе могли быть decode, проверка входа, вычисление, кодирование, вызов за ABI и выход в I/O, но в разговор попало одно слово. Цена такой потери структуры — неверная оптимизация: уменьшить allocation в одном месте и не заметить, что error route изменился, или объяснить внешний вызов свойствами GC.
Разберём один request как учебную цепочку decode → validation → compute → encode. У неё есть trace, result, work units и allocation units. Два варианта меняют только модель временных объектов: один получает 12 units, другой 3, но оба возвращают тот же response и те же 9 work units. Это не benchmark D, не профиль DRuntime и не сравнение языков. Это способ увидеть, какой вопрос нужно отдать измерению позже.
Путь request — это контракт, а не полоса времени
Decode принимает in-memory object и собирает нормализованный request. Validation отделяет неверную операцию от вычисления. Compute складывает два целых числа и оставляет в trace намерения FFI и I/O без их исполнения. Encode превращает успешный result или error в одинаково определённый body. В таком порядке можно спросить, где выросла модель allocation, не смешивая ошибку входа с работой внешней зависимости.
Для valid input мы требуем все шесть следов: decode, validation, compute, ffi-boundary, io-boundary, encode. Для invalid input требуем другой, но тоже явный путь: decode, validation-error, encode-error. Compute и внешние boundaries туда не входят. Это важнее счастливого ответа: если handler молча теряет error route, меньший allocation count не является улучшением.
| Участок | Что записывает fixture | Вопрос к реальной системе | Что нельзя заключить |
|---|---|---|---|
| Decode | shape входа, 2 work units, вариант allocation | какие данные действительно преобразуются на входе? | что HTTP parser или framework работает именно так |
| Validation | accepted либо invalid-operation | какая ветка завершает request до compute? | что validation не создаёт объектов в реальном запуске |
| Compute | операция 19 + 23, 4 work units | какой промежуточный state необходим результату? | что арифметика является bottleneck |
| GC boundary | порог 8 units пересечён только heavy-веткой | где начать фактическую проверку allocation/GC? | наличие, длину или причину pause |
| FFI / I/O | invocation: not-performed | где нужен отдельный contract внешнего вызова? | вызов, скорость, ownership или retry политики |
| Encode | стабильный success/error body | сохранился ли внешний contract endpoint? | поведение реального serializer |
Allocation и GC: отделяем структуру от измерения
В fixture allocation units добавляются не потому, что JavaScript нашёл реальные объекты, а потому, что модель помечает четыре проектных места: decode request shape, temporary state validation, intermediate state compute и response buffer encode. Heavy-вариант записывает 3 + 1 + 5 + 3, reuse-вариант — 1 + 0 + 1 + 1. Сумма задана явно, поэтому review видит, какая правка должна поменять budget.
После пересечения 8 units модель добавляет gc-boundary с полем measurement: "not-a-runtime-pause". Это предохранитель против самой частой подмены: «увидели границу — измерили GC». Официальная документация D действительно обсуждает автоматическое управление памятью и ограничения collector, но из этого не следует наблюдаемая история конкретного request. Нужны фактические условия и отдельный профиль. Наш trace только делает место вопроса видимым.
FFI и I/O должны быть в trace, даже если вызов не выполняется
Внешняя граница часто пропадает из разговора, пока не появится проблема. В учебном request есть ffiIntent: "normalize-scalar" и ioIntent: "write-audit-record". Fixture пишет их как boundaries с invocation: "not-performed". Таким образом код не притворяется, что вызвал C-функцию, открыл файл, ушёл в сеть или увидел процесс. Но будущий интеграционный тест уже знает, где должен появиться owner, contract данных, ошибка и cancellation.
D ABI описывает совместимость с C ABI целевой системы, однако ABI не заменяет договор вызова. У реальной границы отдельно проверяют lifetime аргументов, формат ownership, error code, поток, блокировку, allocator и возможность повторить операцию. Для I/O отдельно нужны protocol, timeout, idempotency, права и данные. Ни один из этих пунктов не выводится из @nogc, из названия DRuntime или из меньшего allocation budget.
Почему «быстро» не переносится между runtime
Слово «быстро» содержит больше переменных, чем кажется. Меняется вход, compiler, версия runtime, link mode, target ABI, операционная система, библиотека, allocator, параллельность и метод измерения. Даже одинаковый source не гарантирует одинаковый путь в другом окружении. Поэтому локальный вывод «reuse имеет 3 units» нельзя превращать в фразу «D быстрее другого runtime». У модели нет секунд, CPU cycles, памяти процесса и внешнего вызова, а значит сравнивать ей нечего.
Историческая рамка здесь тоже нужна. К октябрю 2021 существовал release D 2.097.2; его record и фиксированный source snapshot дают проверяемую точку для терминов DRuntime. Но название версии не заменяет command line, код application или collected evidence. Если нужно сопоставить две среды, сначала фиксируют по одному request и один ожидаемый output, затем одинаково описывают compiler/runtime/platform и только после этого читают реальные результаты.
Минимальный JS-пример: читаем trace, а не придумываем профиль
Следующий фрагмент использует экспортированную fixture. Он не вызывает D API, не открывает сокет и не строит benchmark. Он показывает те trace entries, которые нужны, чтобы отделить успешный path от учебной границы allocation. Если порядок исчезнет, это будет видно до обсуждения реального profiler.
const report = runDRuntimeServiceFixture();
const heavy = report.variants.find((item) => item.variant === "allocation-heavy");
console.log(heavy.trace.map((entry) => entry.stage));
// decode, validation, compute, ffi-boundary, io-boundary, encode ...
console.log(heavy.model);
// { allocationUnits: 12, allocationBudget: 6, withinAllocationBudget: false, ... }
На heavy-ветке trace содержит gc-boundary, обе неисполняемые external boundaries и success encode. На reuse-ветке есть те же handler stages и boundaries, но нет пересечения порога 8. У обоих 9 work units. Это делает проверку строгой: если reuse «выигрывает» только потому, что пропустил validation или encode, assertion о полном пути станет ложным.
Маршрут: симптом → причина → проверка → действие
- Симптом. Зафиксируйте один request, result и error response, которые вызывают вопрос. Не объединяйте их с общими рассказами о нагрузке.
- Причина-гипотеза. Привяжите подозрение к конкретному участку decode, validation, compute, encode, GC boundary, FFI boundary или I/O boundary. «Runtime» не является участком.
- Проверка. Постройте trace valid и invalid inputs. У valid должны быть decode/validation/compute/encode, у invalid — явный error path без compute и external boundaries.
- Проверка budget. Сравните варианты только после равенства body, status и work units. Отдельно покажите, какой из них пересёк выбранный порог.
- Действие. Если изменился только model allocation, готовьте маленькую обратимую правку локального состояния. Если след указывает на FFI/I/O, сначала описывайте contract и тестируйте границу отдельно.
- Следующий шаг. Для фактической производительности соберите реальный профиль с версией compiler/DRuntime, входом, окружением и методом. Не переносите учебный verdict между runtime.
Ограничения и следующий проверяемый шаг
Модель не подтверждает скорость, pause, GC algorithm, масштабирование, throughput, latency, cache behavior, memory footprint, thread scheduling, HTTP semantics, ABI compatibility конкретной библиотеки или эффект compiler flag. В ней нет D compiler, runtime, file, network, foreign code, profiler, benchmark или process-level telemetry. @nogc в источнике не запускается и не используется как оправдание отключить collector.
Практический следующий шаг — перенести названия stages на один реальный handler, не меняя его поведения: сначала получить trace input/result/error, затем выбрать одну границу для независимого измерения. Если граница FFI, добавить отдельный contract test. Если граница I/O, добавить отдельный test timeout/error. Если остался вопрос о allocations, записать точную версию инструмента и повторяемый вход. Так система получает доказательство, а не переносимый ярлык «быстро».
Проверяемые источники
- D 2.097.2: source snapshot of Automatic Memory Management specification — неизменяемый исходник официальной спецификации из тега v2.097.2; он задаёт терминологию automatic memory management, но не профиль конкретного запущенного сервиса
- D 2.097.2: source snapshot of @nogc function specification — неизменяемый исходник официальной спецификации атрибута
@nogc; он ограничивает проверяемый D-код и не доказывает свойства внешней библиотеки или операции ввода-вывода - D 2.097.2: source snapshot of Application Binary Interface specification — неизменяемый исходник официальной границы ABI для взаимодействия с C ABI целевой системы; наличие границы не говорит о стоимости или безопасности конкретного foreign call
- D 2.097.2: официальный release record — выпуск опубликован 9 августа 2021 года и служит проверяемой дооктябрьской точкой для терминов; статья не выводит из номера версии benchmark или runtime profile
- DRuntime 2.097.2: immutable snapshot core/memory.d — точная фиксация исходника DRuntime, на которую указывал тег v2.097.2; используется для исторической проверки терминов, не как замена измерения приложения