Что может сделать С++ RTTI нежелательным?

В документации LLVM они упоминают, что используют "пользовательскую форму RTTI" , и именно по этой причине у них есть isa<>, cast<> и dyn_cast<> шаблонные функции.

Обычно чтение информации о том, что библиотека выполняет некоторые базовые функциональные возможности языка, является ужасным запахом кода и просто приглашает на запуск. Однако это LLVM, о котором мы говорим: ребята работают над компилятором С++ и средой выполнения С++. Если они не знают, что они делают, я довольно сильно напортачил, потому что предпочитаю clang версии gcc, которая поставляется с Mac OS.

Тем не менее, будучи менее опытным, чем они, мне остается недоумевать, каковы подводные камни нормального RTTI. Я знаю, что он работает только для типов, имеющих v-таблицу, но это вызывает только два вопроса:

  • Поскольку вам просто нужен виртуальный метод для создания таблицы vtable, почему бы просто не отметить метод как virtual? Виртуальные деструкторы, похоже, хороши в этом.
  • Если их решение не использует регулярный RTTI, любая идея, как он был реализован?

Ответ 1

Существует несколько причин, по которым LLVM запускает собственную систему RTTI. Эта система проста и мощна и описана в разделе Руководство для программистов LLVM. Как отметил еще один плакат, стандарты кодирования ставит две основные проблемы с С++ RTTI: 1) стоимость пространства и 2) низкую производительность использования он.

Объемная стоимость RTTI довольно высока: каждый класс с vtable (по крайней мере, один виртуальный метод) получает информацию RTTI, которая включает имя класса и информацию о его базовых классах. Эта информация используется для реализации оператора typeid, а также dynamic_cast. Поскольку эта стоимость оплачивается для каждого класса с помощью vtable (и нет, PGO и оптимизация времени соединения не помогают, поскольку vtable указывает на информацию RTTI) LLVM строит с -fno-rtti. Эмпирически это экономит порядка 5-10% от исполняемого размера, что довольно существенно. LLVM не нуждается в эквиваленте typeid, поэтому хранить имена (среди прочего в type_info) для каждого класса - это просто пустая трата пространства.

Плохая производительность довольно проста, если вы проводите бенчмаркинг или смотрите код, сгенерированный для простых операций. Оператор LLVM is & < > обычно компилируется до одной нагрузки и сравнивается с константой (хотя классы контролируют это на основе того, как они реализуют свой метод класса). Вот тривиальный пример:

#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return isa<ConstantInt>(V); }

Скомпилируется для:

$ clang t.cc -S -o - -O3 -I$HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer
...
__Z13isConstantIntPN4llvm5ValueE:
    cmpb    $9, 8(%rdi)
    sete    %al
    movzbl  %al, %eax
    ret

который (если вы не читаете сборку) представляет собой нагрузку и сравнивается с константой. Напротив, эквивалент с dynamic_cast:

#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return dynamic_cast<ConstantInt*>(V) != 0; }

который компилируется до:

clang t.cc -S -o - -O3 -I$HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer
...
__Z13isConstantIntPN4llvm5ValueE:
    pushq   %rax
    xorb    %al, %al
    testq   %rdi, %rdi
    je  LBB0_2
    xorl    %esi, %esi
    movq    $-1, %rcx
    xorl    %edx, %edx
    callq   ___dynamic_cast
    testq   %rax, %rax
    setne   %al
LBB0_2:
    movzbl  %al, %eax
    popq    %rdx
    ret

Это намного больше кода, но убийца - это вызов __dynamic_cast, который затем должен разбираться в структурах данных RTTI и делать очень общий, динамически вычисляемый проход через этот материал. Это на несколько порядков медленнее нагрузки и сравнения.

Хорошо, хорошо, так что это медленнее, почему это имеет значение? Это имеет значение, потому что LLVM выполняет много проверок типов. Многие части оптимизаторов построены вокруг шаблона, сопоставляющего определенные конструкции в коде и выполняющего подстановки на них. Например, вот некоторый код для сопоставления простого шаблона (который уже знает, что Op0/Op1 - левая и правая стороны операции целочисленного вычитания):

  // (X*2) - X -> X
  if (match(Op0, m_Mul(m_Specific(Op1), m_ConstantInt<2>())))
    return Op1;

Оператор соответствия и m_ * являются шаблонами шаблонов, которые сводятся к серии вызовов isa/dyn_cast, каждая из которых должна выполнять проверку типа. Использование dynamic_cast для такого рода мелкозернистого сопоставления образцов было бы жестоким и показательно медленным.

Наконец, есть еще одна точка, которая является одной из выразительности. разные операторы rtti, которые используются LLVM, используются для выражения разных вещей: проверка типа, dynamic_cast, принудительное (утверждение), нулевое обращение и т.д. С++ dynamic_cast не предлагает (изначально) любую из этих функций.

В конце концов, есть два способа взглянуть на эту ситуацию. С другой стороны, С++ RTTI слишком узко определен для того, что многие люди хотят (полное отражение), и слишком медленно, чтобы быть полезным даже для простых вещей, таких как LLVM. С положительной стороны, язык С++ достаточно мощный, чтобы мы могли определять абстракции, подобные этому, как код библиотеки, и отказаться от использования функции языка. Одна из моих любимых вещей о С++ - это то, насколько мощными и элегантными являются библиотеки. RTTI даже не очень высоко среди моих наименее любимых функций С++:)!

-Крис

Ответ 2

Стандарты кодирования LLVM, похоже, достаточно хорошо отвечают на этот вопрос:

Чтобы уменьшить размер кода и исполняемого файла, LLVM не использует RTTI (например, dynamic_cast < > ) или исключения. Эти две особенности языка нарушают общий принцип С++ "вы платите только за то, что используете", вызывая разрастание исполняемого файла, даже если исключения не используются в базе кода или RTTI никогда не используется для класса. Из-за этого мы выключаем их глобально в коде.

Таким образом, LLVM широко использует ручную форму RTTI, которая использует шаблоны, такие как isa < > , cast < > и dyn_cast < > . Эта форма RTTI является опцией и может быть добавлена ​​в любой класс. Это также существенно более эффективно, чем dynamic_cast < > .

Ответ 3

Здесь - отличная статья о RTTI и почему вам может понадобиться перевернуть свою собственную версию.

Я не являюсь экспертом в С++ RTTI, но я также внедрил свой собственный RTTI, потому что есть определенные причины, по которым вам нужно это делать. Во-первых, система С++ RTTI не очень богата функциональностью, в основном все, что вы можете сделать, это литье типов и получение базовой информации. Что, если во время выполнения у вас есть строка с именем класса, и вы хотите построить объект этого класса, удачи вам это удастся с С++ RTTI. Кроме того, С++ RTTI не является (или легко) переносимым по модулю (вы не можете идентифицировать класс объекта, который был создан из другого модуля (dll/so или exe). Аналогично, реализация С++ RTTI специфична для компилятора и обычно это дорого обходится с точки зрения дополнительных накладных расходов, чтобы реализовать это для всех типов. Наконец, он не очень устойчив, поэтому его нельзя действительно использовать для сохранения/загрузки файлов (например, вы можете сохранить данные объекта в файл, но вы также захотите сохранить "typeid" своего класса, чтобы во время загрузки вы знали, какой объект создать для загрузки этих данных, что нельзя сделать надежно с помощью С++ RTTI). По всем или некоторым из этих причин многие фреймворки имеют собственный RTTI (от очень простых до очень богатых функций). Примеры: wxWidget, LLVM, Boost.Serialization и т.д. Это действительно не так уж редко.

Поскольку вам нужен виртуальный метод для работы с таблицей vtable, почему бы просто не пометить метод как виртуальный? Виртуальные деструкторы, похоже, хороши в этом.

Вероятно, что и использует их RTTI. Виртуальные функции являются основой для динамической привязки (привязка во время выполнения), и, следовательно, она в основном требуется для любого типа идентификации/информации типа времени выполнения (а не только для С++ RTTI, но любая реализация RTTI будет иметь так или иначе полагаться на виртуальные вызовы).

Если их решение не использует регулярный RTTI, любая идея, как он был реализован?

Конечно, вы можете искать RTTI-реализации на С++. Я сделал свой собственный, и есть много библиотек, которые также имеют собственный RTTI. На самом деле довольно просто писать. В принципе, все, что вам нужно, - это средство однозначного представления типа (т.е. Имя класса или некоторая измененная его версия или даже уникальный идентификатор для каждого класса), некоторая структура, аналогичная type_info, которая содержит все информацию о типе, который вам нужен, тогда вам понадобится "скрытая" виртуальная функция в каждом классе, которая будет возвращать эту информацию по запросу (если эта функция переопределена в каждом производном классе, она будет работать). Есть, конечно, некоторые дополнительные вещи, которые можно сделать, например, однопользовательский репозиторий всех типов, возможно, со связанными функциями factory (это может быть полезно для создания объектов типа, когда все, что известно во время выполнения, имя типа, в виде строки или идентификатора типа). Кроме того, вы можете добавить некоторые виртуальные функции для динамического тика (обычно это делается путем вызова функции трансляции самого производного класса и выполнения static_cast до типа, который вы хотите отнести).

Ответ 4

Преобладающая причина заключается в том, что они изо всех сил стараются максимально сократить использование памяти.

RTTI доступен только для классов, которые содержат по крайней мере один виртуальный метод, что означает, что экземпляры класса будут содержать указатель на виртуальную таблицу.

В 64-битной архитектуре (которая распространена сегодня) один указатель имеет 8 байтов. Поскольку компилятор создает много мелких объектов, это довольно быстро складывается.

Таким образом, предпринимаются постоянные усилия по максимально возможному (и практическому) удалению виртуальных функций и реализации того, что было бы виртуальными функциями с инструкцией switch, которая имеет схожую скорость выполнения, но значительно снижает влияние на память.

Их постоянное беспокойство по поводу потребления памяти окупилось, поскольку Clang потребляет значительно меньше памяти, чем gcc, например, что важно, когда вы предлагаете библиотеку клиентам.

С другой стороны, это также означает, что добавление нового типа node обычно приводит к редактированию кода в большом количестве файлов, потому что каждый коммутатор необходимо адаптировать (к счастью, компиляторы выдают предупреждение, если вы пропустите член перечисления в переключатель). Поэтому они приняли решение сделать техническое обслуживание еще более трудным во имя эффективности памяти.