Что может сделать C ++ RTTI нежелательным для использования? - PullRequest
67 голосов
/ 27 февраля 2011

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

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

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

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

Ответы [ 4 ]

80 голосов
/ 28 февраля 2011

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

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

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

#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, принудительное (утверждение) приведение, нулевая обработка и т. Д. C ++ dynamic_cast (изначально) не предлагает ни одной из этих функций .

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

-Крис

15 голосов
/ 27 февраля 2011

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

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

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

10 голосов
/ 27 февраля 2011

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

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

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

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

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

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

4 голосов
/ 27 февраля 2011

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

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

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

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

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

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

...