Что может сделать c++ RTTI нежелательным для использования?
глядя на документацию LLVM, они упоминают, что они используют "пользовательскую форму RTTI", и это причина, по которой они имеют isa<>,cast<> и dyn_cast<> шаблонных функций.
обычно, читая, что библиотека переопределяет некоторые основные функции языка, это ужасный запах кода и просто приглашает к запуску. Однако это LLVM, о котором мы говорим: ребята работают над компилятором C++и библиотеки C++. Если они не знают, что они делают, я в значительной степени облажался, потому что я предпочитаю clang до gcc версия, которая поставляется с Mac OS.
тем не менее, будучи менее опытным, чем они, мне остается задаться вопросом, каковы подводные камни нормального RTTI. Я знаю, что он работает только для типов, которые имеют v-таблицу, но это вызывает только два вопроса:
- так как вам просто нужен виртуальный метод, чтобы иметь vtable, почему бы им просто не отметить метод как
virtual? Виртуальные деструкторы, похоже, хороши этот. - если их решение не использует обычный RTTI, есть идеи, как это было реализовано?
4 ответов:
есть несколько причин, почему 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 , 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, принудительное (утверждение) приведение, обработка null и т. д. C++'S dynamic_cast не (изначально) предлагает любую из этих функций.
в конце концов, есть два способа смотреть на эту ситуацию. С другой стороны, C++ RTTI слишком узко определен для того, что хотят многие люди (полное отражение), и слишком медленен, чтобы быть полезным даже для простых вещей, таких как то, что делает LLVM. С положительной стороны, язык C++ достаточно мощный, чтобы мы могли определять такие абстракции как библиотечный код и выбирать использовать функции языка. Одна из моих любимых вещей о C++ - это то, насколько мощными и элегантными могут быть библиотеки. RTTI даже не очень высоко среди моих наименее любимых функций C++ :)!
-Крис
The стандарты кодирования LLVM Кажется, ответить на этот вопрос довольно хорошо:
чтобы уменьшить размер кода и исполняемого файла, LLVM не использует RTTI (например, dynamic_cast) или исключения. Эти две языковые функции нарушают общий принцип C++ "вы платите только за то, что используете", вызывая исполняемое раздувание, даже если исключения никогда не используются в базе кода или если RTTI никогда не используется для класса. Из-за этого, мы поворачиваем их глобально в код.
Что сказал, кода LLVM делает широкое использование самодельных виде библиотеку RTTI, которые используют шаблоны, как Иса литой, и dyn_cast. Эта форма RTTI является опцией и может быть добавлена в любой класс. Это также существенно более эффективно, чем dynamic_cast.
здесь это отличная статья о RTTI и почему вам может понадобиться свернуть свою собственную версию.
Я не эксперт по C++ RTTI, но я также реализовал свой собственный RTTI, потому что есть определенные причины, по которым вам нужно это сделать. Во-первых, система C++ RTTI не очень многофункциональна, в основном все, что вы можете сделать, это ввести литье и получить основную информацию. Что если, во время выполнения, у вас есть строка с именем класса, и вы хотите построить объект этого класса, удачи делать это с C++ RTTI. Кроме того, C++ RTTI не является действительно (или легко) переносимым между модулями (вы не можете определить класс объекта, который был создан из другого модуля (dll/so или exe). Аналогичным образом, реализация RTTI на C++ специфична для компилятора, и обычно дорого включать ее с точки зрения дополнительных накладных расходов для реализации этого для всех типов. Наконец, он не очень постоянен, поэтому его нельзя использовать для сохранения/загрузки файлов, например (например, вы можете сохранить данные объекта в файл, но вы также хотите сохранить "typeid" его класса, так что во время загрузки вы знаете, какой объект создать для загрузки этих данных, что не может быть надежно сделано с помощью C++ RTTI). По всем или некоторым из этих причин многие фреймворки имеют свои собственные RTTI (от очень простых до очень многофункциональных). Примеры: wxWidget, LLVM, Boost.Сериализация и др.. это действительно не такая уж редкость.
С тех пор вам просто нужен виртуальный метод, чтобы иметь vtable, почему бы им просто не пометить метод как виртуальный? Виртуальные деструкторы, похоже, хороши в этом.
это, вероятно, то, что их система RTTI использует тоже. Виртуальные функции являются основой для динамической привязки (привязки времени выполнения), и, таким образом, она в основном требуется для выполнения любого вида идентификации типа времени выполнения / информации (не только требуется C++ RTTI, но и любая реализация RTTI должна будет полагаться на virtual звонит так или иначе).
если их решение не использует обычный RTTI, есть идеи, как это было реализовано?
конечно, вы можете посмотреть реализации RTTI в C++. Я сделал свой собственный и есть много библиотек, которые имеют свои собственные RTTI, а также. Это довольно просто написать, на самом деле. В принципе, все, что вам нужно, это средство для уникального представления типа (т. е. имя класса, или некоторая искаженная версия его, или даже уникальный ID для каждого класса), какая-то структура аналогичная
type_infoкоторый содержит всю информацию о типе, который вам нужен, тогда вам нужна "скрытая" виртуальная функция в каждом классе, которая будет возвращать эту информацию типа по запросу (если эта функция переопределена в каждом производном классе, она будет работать). Есть, конечно, некоторые дополнительные вещи, которые можно сделать, например, одноэлементный репозиторий всех типов, возможно, с соответствующими заводскими функциями (это может быть полезно для создания объектов типа, когда все, что известно во время выполнения-это имя типа, как строка или идентификатор типа). Кроме того, вы можете добавить некоторые виртуальные функции, чтобы разрешить динамическое приведение типов (обычно это делается путем вызова функции приведения наиболее производного класса и выполненияstatic_castдо типа, который вы хотите привести к).
основная причина заключается в том, что они изо всех сил пытаются сохранить использование памяти как можно ниже.
RTTI доступен только для классов, которые имеют по крайней мере один виртуальный метод, что означает, что экземпляры класса будут содержать указатель на виртуальную таблицу.
на 64-битной архитектуре (которая распространена сегодня), один указатель составляет 8 байт. Поскольку компилятор создает множество небольших объектов, это складывается довольно быстро.
поэтому существует постоянные усилия по удалению виртуальных функций как можно больше (и практически), и реализовать то, что было бы виртуальными функциями с
switchинструкция, которая имеет аналогичную скорость выполнения, но значительно меньшее влияние на память.их постоянное беспокойство за потребление памяти окупилось, в этом Clang потребляет значительно меньше памяти, чем gcc, например, что важно, когда вы предлагаете библиотеку клиентам.
С другой стороны, это также означает, что добавление нового типа узла обычно приводит к редактированию кода в большом количестве файлов, потому что каждый коммутатор должен быть адаптирован (к счастью, компиляторы выдают предупреждение, если вы пропустите элемент перечисления в коммутаторе). Поэтому они согласились сделать обслуживание немного сложнее во имя эффективности памяти.
Comments