Сравнение Rust- и C++-версий: замеры и разбор расхождений
Условия, цифры и разбор по коду. Замеры на одной машине подряд: сборка (полная, тёплая, инкрементальная — в release и debug), размеры бинарей и каталогов, состав и объём зависимостей, старт до готовности рендерера, процессорное время при 60 Гц, строки по модулям одинаковыми правилами для обеих версий. Главные наблюдения: * инкрементальная release-сборка Rust 52 с против 4.4 с — это цена thin LTO, а не языка: со сравнимым уровнем оптимизации 5.5 с против 4.4 с; * FetchContent клонирует 775 МБ истории на каждый каталог сборки и не имеет общего кэша; 76 крейтов Rust занимают 80 МБ в общем реестре; * загрузчики .obj сходятся на cow.obj точно и расходятся на plane.obj на 0.13% треугольников — из-за разной триангуляции многоугольников с n >= 5. Плюс четыре дефекта C++-версии, найденных при переносе, в том числе строка «Mesh ready», которую троттлинг категории MeshIO отбрасывает всегда: счётчики после сварки не печатались никогда. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
# Rust против C++: две версии редактора SimVulcan
|
||||
|
||||
Ветка `rust` содержит порт редактора с C++20 на Rust. Обе версии лежат в дереве рядом,
|
||||
собираются независимо и делают одно и то же. Этот документ — не выбор победителя, а
|
||||
разложенные факты: что изменилось, чего это стоило и что померилось.
|
||||
|
||||
## Что именно сравнивается
|
||||
|
||||
Программа одна и та же: окно 1280×720, орбитальная камера вокруг начала координат, три
|
||||
опорные плоскости сетки и цветные оси XYZ, загрузка `.obj` в отдельном потоке, сварка
|
||||
вершин с допуском `1e-4`, три режима отображения модели, единый проход динамического
|
||||
рендеринга с глубинным вложением, submit через `synchronization2`, два кадра в полёте.
|
||||
|
||||
**Общее:** каталог `shaders/` — обе версии компилируют одни и те же `.vert`/`.frag` тем
|
||||
же `glslangValidator -V --target-env vulkan1.3`; каталог `assets/` — одни и те же модели.
|
||||
|
||||
**Разное по договорённости:** интерфейс. В C++ это Dear ImGui, в Rust — **egui**. Крейт
|
||||
`imgui` существует, но это биндинги: сборка всё равно тянула бы ~40 тыс. строк C++ через
|
||||
`cc`, и «Rust-версия», компилирующая C++, обесценила бы сравнение экосистем. Панели
|
||||
из-за этого выглядят иначе; набор элементов управления тот же.
|
||||
|
||||
## Условия замеров
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| машина | Intel Core i5-1135G7, 4 ядра / 8 потоков, 16 ГБ, SSD |
|
||||
| видео | Intel Iris Xe, драйвер 31.0.101.5186, экран 60 Гц |
|
||||
| ОС | Windows 11 Home 10.0.26200 |
|
||||
| C++ | MSVC 14.50.35717 (VS 18 BuildTools), CMake 4.3.2, Ninja 1.13.2 |
|
||||
| Rust | rustc / cargo 1.97.1, MSVC-ABI |
|
||||
| общее | Vulkan SDK 1.4.341.1 |
|
||||
|
||||
Обе сборки — релизные (`CMAKE_BUILD_TYPE=Release`; `cargo --release` с `lto = "thin"`,
|
||||
`codegen-units = 1`). Обе однопоточно ограничены одним и тем же железом; замеры сделаны
|
||||
подряд, без других нагрузок.
|
||||
|
||||
## Цифры
|
||||
|
||||
### Сборка
|
||||
|
||||
Все числа C++ уменьшены на 3.0 с — накладные расходы `vcvars64.bat`, который приходится
|
||||
вызывать в каждом сеансе (замерены отдельно, среднее из трёх). У `cargo` такого нет.
|
||||
|
||||
| | C++ | Rust |
|
||||
|---|---|---|
|
||||
| конфигурация (исходники зависимостей на месте) | 9.7 с | входит в сборку |
|
||||
| **release с нуля** | **114 с** | **194 с** (thin LTO) · 182 с (без LTO) |
|
||||
| release, ничего не менялось | 1.1 с | 1.0 с |
|
||||
| **release после правки одного файла** | **4.4 с** | **52.3 с** (thin LTO) · 5.5 с (без LTO) |
|
||||
| debug с нуля | 112 с | 83 с |
|
||||
| debug после правки одного файла | 8.0 с | 5.2 с |
|
||||
|
||||
Правился в обоих случаях самый крупный файл Vulkan-слоя: `src/vk/Renderer.cpp` и
|
||||
`crates/simv-vk/src/renderer.rs`.
|
||||
|
||||
Две оговорки, без которых таблица врёт.
|
||||
|
||||
**Уровни оптимизации разные.** `CMAKE_BUILD_TYPE=Release` у MSVC — это `/O2` без
|
||||
оптимизации всей программы (CMake не включает `INTERPROCEDURAL_OPTIMIZATION` сам).
|
||||
Профиль порта просит `lto = "thin"` и `codegen-units = 1`, то есть межкрейтовую
|
||||
оптимизацию. Отсюда и 52 секунды на инкрементальную пересборку: правка одной строки
|
||||
заставляет заново оптимизировать и слинковать весь бинарь. Колонка «без LTO» —
|
||||
`CARGO_PROFILE_RELEASE_LTO=false`, `CODEGEN_UNITS=16`, то есть примерный аналог `/O2`
|
||||
без LTCG: **5.5 с против 4.4 с**, и разницы уже нет. Инкрементальная сборка в Rust
|
||||
медленная не сама по себе, а ровно настолько, насколько её просят оптимизировать.
|
||||
|
||||
**«С нуля» здесь не значит «с чистой машины».** Обе цифры сняты при уже скачанных
|
||||
зависимостях. Честная холодная сборка C++ до конца не дошла: `FetchContent` клонирует
|
||||
девять репозиториев с полной историей, и на этом соединении она за пятнадцать минут
|
||||
успела скачать два из них (glfw 24 МБ, glm 100 МБ) и была прервана. Готовое дерево
|
||||
зависимостей от отладочной сборки занимает **775 МБ**, и общего кэша у FetchContent нет:
|
||||
каждый новый каталог сборки скачивает всё заново. Реестр cargo, наоборот, один на
|
||||
пользователя.
|
||||
|
||||
### Артефакты и зависимости
|
||||
|
||||
| | C++ | Rust |
|
||||
|---|---|---|
|
||||
| `SimVulcan.exe`, release | **1.18 МБ** | **5.80 МБ** |
|
||||
| `SimVulcan.exe`, debug | 5.68 МБ | 28.97 МБ |
|
||||
| каталог сборки | 78 МБ | 469 МБ |
|
||||
| объявлено зависимостей | 9 | 15 |
|
||||
| всего пакетов в графе | 10 | **76** |
|
||||
| исходники зависимостей на диске | **775 МБ** на каждый каталог сборки | **80 МБ** в общем реестре |
|
||||
|
||||
Пятикратная разница в размере бинаря — это в основном статически влинкованная стандартная
|
||||
библиотека Rust и форматирование `core::fmt`; C++-версия тянет CRT из системы. Разница в
|
||||
числе пакетов (76 против 10) впечатляет ровно до того момента, как посмотреть на объём:
|
||||
76 крейтов Rust занимают в восемь раз меньше места, чем девять библиотек C++, потому что
|
||||
`.crate` — это архив выпуска, а `FetchContent` — клон с историей.
|
||||
|
||||
### Выполнение
|
||||
|
||||
Обе версии показывают в режиме FIFO на экране 60 Гц, поэтому частота кадров у них
|
||||
одинаковая по построению и ничего не измеряет. Сравнимы старт и процессорная стоимость.
|
||||
|
||||
| | C++ | Rust |
|
||||
|---|---|---|
|
||||
| старт до готовности рендерера (лучший из трёх) | 1068 мс | **754 мс** |
|
||||
| то же, средний | 1080 мс | 872 мс* |
|
||||
| процессорное время при 60 Гц | **11.2%** реального | 12.6% реального |
|
||||
|
||||
\* без первого запуска сразу после сборки: он занял 3.4 с, пока система подтягивала
|
||||
свежий 5.8-мегабайтный образ с диска. У C++-версии разброс между запусками — 21 мс.
|
||||
|
||||
Полтора процента разницы по процессору — это egui против Dear ImGui, а не Rust против
|
||||
C++: egui пересобирает раскладку каждый кадр, ImGui хранит её между кадрами. Сцена
|
||||
(сетка + модель) в обеих версиях записывается одинаковыми вызовами.
|
||||
|
||||
### Совпадают ли результаты
|
||||
|
||||
Проверка, что порт считает то же самое: один и тот же файл загружается обеими версиями,
|
||||
сравниваются счётчики.
|
||||
|
||||
| файл | C++ (tinyobjloader) | Rust (tobj) |
|
||||
|---|---|---|
|
||||
| `cow.obj` | 2451 вершины, 4898 треугольников | **2451, 4898** |
|
||||
| `plane.obj` | 10673 вершины, 20035 треугольников | 10638, **20061** |
|
||||
|
||||
`cow.obj` сходится точно. `plane.obj` расходится, и оба расхождения объяснимы:
|
||||
|
||||
* **35 вершин.** tinyobjloader отдаёт глобальный массив `attrib.vertices` целиком, включая
|
||||
вершины, на которые не ссылается ни одна грань; tobj такие отбрасывает. Сварка потом
|
||||
всё равно оставила бы их висеть в списке — на картинку они не влияют.
|
||||
* **26 треугольников.** В `plane.obj` 121 грань с пятью и более вершинами. tobj режет
|
||||
n-угольник веером, что даёт ровно `n − 2` треугольника — суммарно 20061, и это
|
||||
совпадает с прямым подсчётом по файлу. tinyobjloader для `n ≥ 5` применяет
|
||||
earcut-подобную триангуляцию и выбрасывает выродившиеся треугольники, отсюда 20035.
|
||||
Разница 0.13% и целиком лежит в библиотеке чтения, а не в переносе.
|
||||
|
||||
Сварка вершин (`weld_vertices`) перенесена построчно, включая бакетирование округлением
|
||||
и FNV-хеш; три её теста из Catch2 перенесены дословно и проходят.
|
||||
|
||||
### Побочная находка: строка, которой не бывает
|
||||
|
||||
При сверке счётчиков выяснилось, что C++-версия **никогда не печатает** результат сварки.
|
||||
`ObjLoader::LoadObj` пишет `Loaded OBJ '…': N verts, M tris` в категорию `MeshIO`, а
|
||||
обработчик в `main.cpp` сразу за ним пишет туда же `Mesh ready: …`. У категории стоит
|
||||
троттлинг 0.5 с, оба сообщения уровня `Info` — второе гарантированно отбрасывается.
|
||||
В журналах обеих загрузок (`cow.obj` и `plane.obj`) строки `Mesh ready` нет.
|
||||
|
||||
Это и стало причиной единственного расхождения в переносе журнала: в порте троттлинг по
|
||||
умолчанию выключен, а `set_throttle` включается там, где сообщения идут покадрово.
|
||||
Иначе стартовый вывод терял бы имя видеокарты — в C++ он уцелел лишь потому, что
|
||||
низкоуровневый код Vulkan пишет напрямую через `spdlog`, минуя категории.
|
||||
|
||||
## Строки кода
|
||||
|
||||
Считано одинаковыми правилами для обеих версий: строка комментарийная, если после
|
||||
обрезки пробелов начинается с `//` (для C++ ещё блочные `/* … */`); хвостовые
|
||||
комментарии за кодом не вычитаются.
|
||||
|
||||
| модуль | C++ | | | | Rust | | | |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| | всего | пусто | комм. | **кода** | всего | пусто | комм. | **кода** |
|
||||
| журнал, окно (`core`) | 370 | 83 | 11 | **276** | 310 | 36 | 62 | **212** |
|
||||
| Vulkan (`vk`) | 2120 | 380 | 88 | **1652** | 2491 | 238 | 255 | **1998** |
|
||||
| меш (`mesh`) | 236 | 50 | 14 | **172** | 395 | 50 | 70 | **275** |
|
||||
| редактор (`editor`) | 305 | 68 | 20 | **217** | 431 | 48 | 62 | **321** |
|
||||
| приложение (`app`) | 85 | 14 | 5 | **66** | 226 | 27 | 43 | **156** |
|
||||
| тесты | 65 | 9 | 5 | **51** | *внутри модулей* | | | *≈150* |
|
||||
| **итого** | **3181** | 604 | 143 | **2434** | **3853** | 399 | 492 | **2962** |
|
||||
| система сборки | 431 | | | **399** | 232 | | | **197** |
|
||||
|
||||
Порт длиннее примерно на пятую часть, и перевес почти весь в Vulkan-слое: +346 строк кода
|
||||
в `simv-vk` — это написанная руками замена vk-bootstrap. Приложение выросло с 66 до 156
|
||||
строк, потому что в него переехал `App` из `simv_core` (см. расхождение 1) и поиск
|
||||
каталога моделей стал честным обходом предков вместо пяти захардкоженных `../`.
|
||||
|
||||
Комментариев в порте втрое больше (492 против 143), и это перекос замера, а не свойство
|
||||
языка: значительная их часть — пометки «здесь расхождение с C++-версией и вот почему»,
|
||||
написанные ради этого документа. По строкам собственно кода разрыв — 2962 против 2434,
|
||||
и он меньше, если вычесть тесты: они в Rust живут внутри модулей (≈150 строк), в C++ —
|
||||
отдельной целью (51 строка).
|
||||
|
||||
## Структурные расхождения
|
||||
|
||||
Каждое — с причиной. Ни одно не сделано «чтобы было красивее».
|
||||
|
||||
### 1. `App` переехал в исполняемый крейт
|
||||
|
||||
В C++ граф целей цикличен: `core::App` владеет `vk::Renderer`, а `vk::Renderer`
|
||||
принимает `core::Window&`. Работает это только потому, что `simv_vk` **не линкует**
|
||||
`simv_core`, а видит его заголовки через `target_include_directories(simv_vk PUBLIC ..)`,
|
||||
и всё сходится на компоновке исполняемого файла. Cargo цикл между крейтами отвергает
|
||||
сразу, поэтому `App` (25 строк) поднят на уровень выше обоих — в `simv-app`.
|
||||
|
||||
Это единственный пункт, где Rust потребовал перекладывать код, и заодно единственный,
|
||||
где он указал на настоящую проблему в исходной раскладке: срез слоёв, который CMake
|
||||
пропустил молча.
|
||||
|
||||
### 2. Границу модулей проверяет компилятор
|
||||
|
||||
Главный инвариант проекта — «весь Vulkan живёт в `src/vk/`» — в C++ держится на
|
||||
дисциплине и записи в `CLAUDE.md`: ничто не мешает написать `#include <volk.h>` в
|
||||
`mesh/ObjLoader.cpp`, все заголовки видны. В порте `simv-mesh` и `simv-editor` просто не
|
||||
имеют `ash` в зависимостях: нарушение не соберётся.
|
||||
|
||||
Побочный эффект: `EditorUI` не может назвать тип `vk::RenderMode`. C++ обходит ту же
|
||||
границу, возвращая из панели голый `int`, который `main.cpp` приводит
|
||||
`static_cast<vk::RenderMode>` — связь двух перечислений там держится на честном слове.
|
||||
В порте у панели свой `DisplayMode`, а разбор делает `match` в `simv-app`: добавится
|
||||
вариант — компилятор укажет место.
|
||||
|
||||
### 3. pImpl не понадобился
|
||||
|
||||
`vk::Renderer` в C++ прячет всё за `struct Renderer::Impl` и `unique_ptr` ради одного:
|
||||
чтобы заголовок не тащил volk в трансляционные единицы `App` и `main`. В Rust заголовков
|
||||
нет, и приватные поля модуля дают ту же изоляцию бесплатно — минус один уровень
|
||||
косвенности и минус ручное объявление/определение `Impl` на 45 строк.
|
||||
|
||||
### 4. Замыкание интерфейса стало параметром, а состояние — возвратом
|
||||
|
||||
В C++ `Renderer` хранит `std::function<void()>`, а замыкание из `main.cpp` захватывает по
|
||||
ссылке камеру, панели и **сам рендерер**, чтобы вызвать `SetViewProj`, `SetRenderMode`,
|
||||
`SetGridVisible`. В Rust так нельзя: замыкание вызывается из метода рендерера, то есть
|
||||
рендерер оказался бы одолжен дважды.
|
||||
|
||||
Решение: замыкание передаётся параметром в `draw_frame` и **возвращает** `FrameState`
|
||||
с матрицей вида-проекции, режимом и видимостью сетки. Состояние применяется к тому же
|
||||
кадру, отставания нет. Сеттеры исчезли за ненадобностью.
|
||||
|
||||
```cpp
|
||||
// C++: состояние ставится изнутри колбэка
|
||||
app.SetUiCallback([&]() {
|
||||
editorUI.Draw(camera);
|
||||
renderer.SetViewProj(camera.ViewProj(renderer.AspectRatio()));
|
||||
});
|
||||
```
|
||||
|
||||
```rust
|
||||
// Rust: состояние возвращается из замыкания
|
||||
renderer.draw_frame(window, |ctx| {
|
||||
editor_ui.draw(ctx, camera);
|
||||
FrameState { view_proj: camera.view_proj(aspect), .. }
|
||||
})?;
|
||||
```
|
||||
|
||||
### 5. Выгрузка меша уехала из середины кадра
|
||||
|
||||
В C++ панель загрузки зовёт `OnLoaded` прямо из `Draw`, то есть из середины
|
||||
`Renderer::DrawFrame`, **уже после** `vkAcquireNextImageKHR`. Обработчик оттуда дёргает
|
||||
`SetMeshCpu`, а тот — `vkDeviceWaitIdle` и выгрузку на видеокарту. Законно, но ждать
|
||||
простоя устройства посреди записи кадра — сомнительно.
|
||||
|
||||
В Rust это невозможно по той же причине, что и пункт 4. Панель складывает готовый меш в
|
||||
поле, приложение забирает его после кадра. Ограничение языка здесь вынесло тяжёлую
|
||||
операцию из горячего пути — не потому, что кто-то это заметил.
|
||||
|
||||
### 6. `Buffer` и `Image` перестали быть мёртвым грузом
|
||||
|
||||
В C++ обе обёртки собираются в цель `simv_vk`, но **ими не пользуется никто**: и
|
||||
`GridRenderer`, и `GpuMesh`, и глубинное вложение `Renderer` создают ресурсы прямыми
|
||||
вызовами VMA. Это 200 строк, которые компилируются каждую сборку и никогда не работают.
|
||||
В порте на них построены все три потребителя — не из-за языка, а потому что порт писался,
|
||||
когда потребители уже известны. С gpu-allocator обёртка ещё и обязательна по существу:
|
||||
она держит `Allocation`, который иначе пришлось бы освобождать руками в каждом `Drop`.
|
||||
|
||||
### 7. Асинхронная загрузка: `mpsc` вместо `std::future`
|
||||
|
||||
`LoadObjAsync` в C++ возвращает `std::future<Mesh>`, и исключение из рабочего потока
|
||||
всплывает при `get()`. В порте поток шлёт `Result<Mesh, ObjError>` по каналу, панель
|
||||
опрашивает `try_recv()`. Разница не в количестве кода (его столько же), а в том, что
|
||||
ошибка стала значением: пропустить её нельзя, а тип в сигнатуре перечисляет, что вообще
|
||||
может пойти не так.
|
||||
|
||||
### 8. SPIR-V вшит в исполняемый файл
|
||||
|
||||
`build.rs` компилирует шейдеры в `OUT_DIR`, откуда они попадают в бинарь через
|
||||
`include_bytes!`. Вместе с этим исчезли: функция `FindSpvPath` с перебором путей, два
|
||||
POST_BUILD-шага копирования каталогов в `CMakeLists.txt`, требование запускать редактор
|
||||
из его собственного каталога и целый класс ошибок «шейдер не найден» во время выполнения
|
||||
— отсутствующий шейдер теперь ломает сборку. Из путевых зависимостей остался только
|
||||
`assets/meshes`, и он ищется от бинаря, а не от текущего каталога.
|
||||
|
||||
### 9. Цикл событий принадлежит библиотеке
|
||||
|
||||
GLFW отдаёт цикл приложению (`while (!ShouldClose()) { PollEvents(); DrawFrame(); }`),
|
||||
winit забирает его себе и зовёт приложение через `ApplicationHandler`. `Window::ShouldClose`
|
||||
и `Window::PollEvents` исчезли, их место заняли `window_event` и `about_to_wait`. Это
|
||||
свойство winit, а не Rust: крейт `glfw` существует и сохранил бы прежнюю форму, но он
|
||||
такие же биндинги к C, как `imgui`.
|
||||
|
||||
## Чего в экосистеме Rust не нашлось
|
||||
|
||||
**Замены vk-bootstrap нет.** В C++ выбор физического устройства по требуемым
|
||||
возможностям, поиск семейств очередей, проверка наличия слоя валидации и сборка цепочки
|
||||
показа — это `vkb::InstanceBuilder`, `vkb::PhysicalDeviceSelector`, `vkb::SwapchainBuilder`,
|
||||
суммарно около сорока строк вызовов. В порте это `context.rs` и `swapchain.rs`, написанные
|
||||
руками: перебор устройств, чтение `PhysicalDeviceFeatures2` с цепочкой `Vulkan12/13Features`,
|
||||
разбор флагов семейств очередей, проверка поддержки показа на поверхность, выбор формата
|
||||
с откатом, зажим размера и числа образов по возможностям поверхности. Это **самая
|
||||
большая статья расходов порта** и главная причина, по которой `simv-vk` вышел длиннее
|
||||
своего прообраза.
|
||||
|
||||
**Ритм выпусков разный.** `ash` 0.38 вышел в апреле 2024 и с тех пор не обновлялся —
|
||||
это не заброшенность (Vulkan 1.3 он покрывает полностью, а генерируемый API стабилен),
|
||||
но зафиксировать стоит. `egui` 0.36 и `egui-ash-renderer` 0.13 — август 2026, живее
|
||||
некуда, причём вторая обновилась вслед за первой за две недели. Обратная сторона живости
|
||||
— поломки: `egui` 0.36 переименовал `Context::run` в `run_ui`, сменил форму
|
||||
`TexturesDelta` (карта правок вместо одной на текстуру) и объявил `set_textures`
|
||||
устаревшим. Порт писался под 0.36 и учитывает всё это; годичной давности пример из сети
|
||||
не собрался бы.
|
||||
|
||||
**Мелочи, которые в C++ приходят с библиотекой.** spdlog печатает локальное время из
|
||||
коробки; в Rust за форматированной меткой пришлось взять `chrono`. GLM определяется
|
||||
одним заголовком; glam — крейт, зато соглашение о пространстве отсечения у него вынесено
|
||||
в путь функции (`camera::rh::proj::vulkan::perspective` вместо
|
||||
`GLM_FORCE_DEPTH_ZERO_TO_ONE` плюс ручной `p[1][1] *= -1`).
|
||||
|
||||
## Где какой язык помог, а где помешал
|
||||
|
||||
**Rust помешал — заимствования на границе рендерера.** Пункты 4 и 5 выше: конструкция
|
||||
«объект хранит замыкание, которое трогает этот же объект» в C++ пишется не думая, а в
|
||||
Rust не компилируется вовсе. Обход занял полчаса и, если честно, дал код лучше исходного
|
||||
— но полчаса были потрачены на спор с компилятором, а не на задачу.
|
||||
|
||||
**Rust помешал — цепочки pNext.** `push_next` держит `&mut` на вложенную структуру, пока
|
||||
жива голова цепочки, поэтому прочитать `f13.synchronization2` при живом
|
||||
`PhysicalDeviceFeatures2` компилятор не даёт. В C++ pNext — сырой указатель, и вопроса
|
||||
нет. Лечится блоком, который гасит заимствование:
|
||||
|
||||
```rust
|
||||
let (f13, f12, f10) = {
|
||||
let mut f13 = vk::PhysicalDeviceVulkan13Features::default();
|
||||
let mut f2 = vk::PhysicalDeviceFeatures2::default().push_next(&mut f13) /* … */;
|
||||
unsafe { instance.get_physical_device_features2(handle, &mut f2) };
|
||||
(f13, /* … */ f2.features)
|
||||
};
|
||||
```
|
||||
|
||||
**Rust помог — порядок уничтожения.** В C++ `Renderer::Impl::~Impl` — это тридцать строк
|
||||
ручного разрушения в правильном порядке, и любая перестановка полей в объявлении структуры
|
||||
её не сломает, но и не поможет: связи нет. В Rust порядок полей **и есть** порядок
|
||||
уничтожения, а `Drop` у каждой обёртки снимает вопрос «а это уже освободили?». Ловушка
|
||||
осталась одна и она подписана в коде: `ash::Device` клонируется как таблица функций, а не
|
||||
как владеющая ссылка, поэтому контекст обязан объявляться последним.
|
||||
|
||||
**Rust помог — ошибки как значения.** `CheckVk(r, where)` в `Renderer.cpp` — это ручная
|
||||
проверка каждого вызова с бросанием `runtime_error`. В порте её место занял `?` и
|
||||
трейт-расширение на четыре строки, навешивающее имя вызова. Пропустить проверку нельзя:
|
||||
`#[must_use]` на `VkResult` не даст.
|
||||
|
||||
**Rust помог — незагруженный меш.** В C++ это `GpuMesh` с состоянием «пустой», метод
|
||||
`IsValid()` и отдельный флаг `hasMesh` в `Renderer`, которые обязаны быть согласованы.
|
||||
В порте — `Option<GpuMesh>`, и рассогласовать нечего.
|
||||
|
||||
**Ничья — сам Vulkan.** Записи `vkCmdBeginRendering`, барьеры sync2, сборка конвейеров
|
||||
выглядят в обеих версиях почти одинаково, строка в строку. `ash` не пытается быть
|
||||
безопасным поверх Vulkan, и это правильно: `unsafe`-блоки локализуют ровно те места, где
|
||||
C++ тоже опасен, не притворяясь, что их нет.
|
||||
|
||||
## Итог
|
||||
|
||||
Ни один из замеров не даёт разницы в порядок величины, так что решение к таблице не
|
||||
сводится. Что можно утверждать по итогам порта:
|
||||
|
||||
**Порт занял примерно на пятую часть больше строк**, и почти весь перевес пришёлся на
|
||||
`simv-vk`. Причина одна и она названа выше: замены vk-bootstrap нет, и сорок строк
|
||||
вызовов превратились в двести пятьдесят строк выбора устройства и сборки цепочки показа.
|
||||
Вне Vulkan-слоя объёмы сопоставимы. Если проект растёт дальше именно в Vulkan-часть,
|
||||
перевес разовый: инициализация пишется один раз.
|
||||
|
||||
**Сборка: паритет везде, кроме одной клетки.** Полная сборка release у C++ вдвое быстрее
|
||||
(114 с против 194 с), полная debug — наоборот, медленнее (112 с против 83 с),
|
||||
инкрементальная в debug у Rust быстрее (5.2 с против 8.0 с). Единственная резкая
|
||||
клетка — инкрементальная release: 52 с против 4.4 с. Это цена thin LTO, а не языка: со
|
||||
сравнимым уровнем оптимизации (LTO выключен, как у MSVC `/O2` без LTCG) те же 5.5 с
|
||||
против 4.4 с. Практический вывод простой — держать в профиле разработки LTO выключенным,
|
||||
включая его только для выпуска.
|
||||
|
||||
**Четыре дефекта в C++-версии нашлись при переносе, и ни один не искали.** Цикл в графе
|
||||
целей CMake, который проходит только из-за незалинкованной цели. Выгрузка меша с
|
||||
`vkDeviceWaitIdle` посреди записи кадра. Рассогласование `GpuMesh::IsValid()` и флага
|
||||
`hasMesh` — два источника правды об одном. Строка `Mesh ready`, которую троттлинг
|
||||
категории `MeshIO` отбрасывает всегда, так что счётчики после сварки не печатались
|
||||
никогда. Первое стало ошибкой сборки, второе — невозможным, третье — невыразимым,
|
||||
четвёртое всплыло при сверке чисел.
|
||||
|
||||
**И двести строк мёртвого кода.** `Buffer` и `Image` собираются каждую сборку и никем не
|
||||
используются. Это не про язык, а про то, что переписывание само по себе работает как
|
||||
аудит.
|
||||
|
||||
**Цена — время на границах.** Замыкание, которое трогает объект, из чьего метода оно
|
||||
вызвано; цепочки pNext, держащие заимствование; порядок уничтожения полей. Каждое место
|
||||
решается за полчаса, каждое решение оказывается не хуже исходного, но полчаса уходят на
|
||||
разговор с компилятором, а не на задачу.
|
||||
|
||||
**Экосистема разнородна.** `egui` и его Vulkan-бэкенд выпускаются раз в месяц и ломают
|
||||
API; `ash` не менялся два года и не собирается. Замены vk-bootstrap нет вовсе. В C++
|
||||
FetchContent тянет девять библиотек по фиксированным тегам и не имеет общего кэша: каждый
|
||||
свежий каталог сборки скачивает их заново.
|
||||
|
||||
Выбор языка — за вами. Этот документ фиксирует только то, что удалось измерить и показать
|
||||
на коде.
|
||||
Reference in New Issue
Block a user