Rust: Почему dyn Trait на самом деле медленнее дженериков — не только из-за vtable
Разработчики Rust часто объясняют разницу в производительности между `dyn Trait` и дженериками одним фактором: косвенным вызовом через таблицу виртуальных методов (vtable). Этот ответ верен лишь отчасти. Реальная цена динамической диспетчеризации скрыта не в самом прыжке по указателю, а в том, что этот вызов становится непреодолимым барьером для оптимизатора компилятора. Когда LLVM видит непрозрачный вызов по указателю, он не может встроить тело функции, раскрутить цикл или протащить константы через границу вызова. Один косвенный вызов блокирует целый каскад потенциальных оптимизаций.
Чтобы понять корень проблемы, необходимо заглянуть внутрь реализации `dyn Trait`. Этот механизм скрывает за собой «толстый указатель», состоящий из двух частей: указателя на данные и указателя на vtable. Сама vtable в памяти представляет собой массив указателей на функции, соответствующие методам трейта. Именно эта структура делает вызов полиморфным, но и создает непрозрачность для компилятора. В отличие от этого, дженерики в Rust используют мономорфизацию — для каждого конкретного типа генерируется специализированная версия кода, что делает все вызовы статически известными на этапе компиляции.
Таким образом, разница в скорости — это не просто «один лишний переход». Это фундаментальное различие в видимости для оптимизатора. Использование `dyn Trait` там, где типы известны на этапе компиляции, лишает код значительной части оптимизаций, которые могли бы быть применены. Понимание внутреннего устройства этих механизмов критически важно для написания высокопроизводительного кода на Rust и осознанного выбора между абстракцией и скоростью.