Надеюсь, это не слишком специализированный вопрос для StackOverflow: если он существует и может быть перенесен в другое место, дайте мне знать...
Много лун назад я написал дипломную работу, в которой предлагал различные методы девиртуализации для C++ и родственных языков, обычно основанные на идее предварительно скомпилированной специализации путей кода (что-то вроде шаблонов), но с проверками для выбора правильной специализации, выбираемой во время выполнения в тех случаях, когда они не могут быть выбраны во время компиляции (как и должны быть шаблоны).
(Очень) основная идея выглядит примерно так: предположим, у вас есть класс C
, подобный следующему:
class C : public SomeInterface
{
public:
C(Foo * f) : _f(f) { }
virtual void quack()
{
_f->bark();
}
virtual void moo()
{
quack(); // a virtual call on this because quack() might be overloaded
}
// lots more virtual functions that call virtual functions on *_f or this
private:
Foo * const _f; // technically doesn't have to be const explicitly
// as long as it can be proven not be modified
};
И вы знали, что существуют конкретные подклассы Foo
, такие как FooA
, FooB
и т. д., с известными полными типами (не обязательно имея исчерпывающий список), тогда вы могли бы предварительно скомпилировать специализированные версии C
для некоторых выбранных подклассов Foo
, например, ( обратите внимание, что конструктор не включен здесь намеренно, так как он не будет вызываться):
class C_FooA final : public SomeInterface
{
public:
virtual void quack() final
{
_f->FooA::bark(); // non-polymorphic, statically bound
}
virtual void moo() final
{
C_FooA::quack(); // also static, because C_FooA is final
// _f->FooA::bark(); // or you could even do this instead
}
// more virtual functions all specialized for FooA (*_f) and C_FooA (this)
private:
FooA * const _f;
};
И замените конструктор C
чем-то вроде следующего:
C::C(Foo * f) : _f(f)
{
if(f->vptr == vtable_of_FooA) // obviously not Standard C++
this->vptr = vtable_of_C_FooA;
else if(f->vptr == vtable_of_FooB)
this->vptr = vtable_of_C_FooB;
// otherwise leave vptr unchanged for all other values of f->vptr
}
Таким образом, динамический тип создаваемого объекта изменяется на основе динамического типа аргументов его конструктора. (Обратите внимание, что вы не можете сделать это с шаблонами, потому что вы можете создать C<Foo>
, только если вы знаете тип f
во время компиляции). Отныне любой вызов с FooA::bark()
по C::quack()
включает только один виртуальный вызов: либо вызов C::quack()
статически привязывается к неспециализированной версии, которая динамически вызывает FooA::bark()
, либо вызов C::quack()
динамически перенаправляется на C_FooA::quack()
, который статически вызывает FooA::bark()
. Кроме того, в некоторых случаях динамическая диспетчеризация может быть полностью устранена, если анализатор потока имеет достаточно информации для статического вызова C_FooA::quack()
, что может быть очень полезно в тесном цикле, если он допускает встраивание. (Хотя технически на тот момент вы, вероятно, были бы в порядке даже без этой оптимизации...)
(Обратите внимание, что это преобразование безопасно, хотя и менее полезно, даже если _f
не является константным и защищенным, а не частным, а C
наследуется от другой единицы перевода... единица перевода, создающая виртуальную таблицу для унаследованного класса, не будет знать вообще ничего о специализациях и конструкторе унаследованного класса просто установит this->vptr
в свою собственную виртуальную таблицу, которая не будет ссылаться ни на какие специализированные функции, потому что ничего о них не знает.)
Может показаться, что для устранения одного уровня косвенности требуется много усилий, но дело в том, что вы можете сделать это для любого произвольного уровня вложенности (любая глубина виртуальных вызовов, следующих этому шаблону, может быть уменьшена до одного), основываясь только на локальной информации внутри единицы перевода, и сделать это устойчивым образом, даже если новые типы определены в других единицах перевода, о которых вы не знаете... вы просто можете добавить много кода, которого у вас не было бы в противном случае, если бы вы сделал это наивно.
В любом случае, независимо от того, будет ли такая оптимизация действительно иметь достаточную отдачу, стоит ли усилий по реализации, а также стоит ли накладных расходов на пространство в результирующем исполняемом файле, мой вопрос заключается в том, есть ли что-нибудь в стандартном С++, что помешало бы компилятору выполнить такое преобразование?
Мне кажется, что нет, поскольку стандарт вообще не определяет, как выполняется виртуальная диспетчеризация или как представляются указатели на функции-члены. Я почти уверен, что в механизме RTTI нет ничего, что мешало бы C
и C_FooA
маскироваться под один и тот же тип для всех целей, даже если у них разные виртуальные таблицы. Единственная другая вещь, которая, как я мог подумать, может иметь значение, — это внимательное прочтение ODR, но, вероятно, нет.
Я что-то упускаю из виду? За исключением проблем с ABI/связыванием, возможны ли подобные преобразования без нарушения соответствующих программ на C++? (Кроме того, если да, можно ли это сделать в настоящее время с помощью Itanium и/или MSVC ABI? Я вполне уверен, что ответ тоже да, но, надеюсь, кто-то может это подтвердить.)
EDIT: кто-нибудь знает, реализовано ли что-то подобное в каком-либо основном компиляторе/JIT для C++, Java или C#? (См. обсуждение и связанный чат в комментариях ниже...) Я знаю, что JIT выполняют спекулятивное статическое связывание/встраивание виртуальных машин непосредственно на сайтах вызовов, но я не знаю, делают ли они что-то подобное (с совершенно новыми vtables генерируется и выбирается на основе проверки одного типа, выполняемой в конструкторе, а не в каждом месте вызова).
C_FooX
, но вы затем статически вызываете правильныйFooX
. Или вы статически вызываетеC
и динамически получаете правильныйFooX
. Два виртуальных вызова вместо одного, и вы можете сделать это с любым уровнем вложенности, если информация о типе находится в текущей TU, и она устойчива к добавлению дополнительных типов в другие TU (поэтому вам не нужен анализ всей программы). ). - person Stephen Lin   schedule 01.03.2013foo_->foo();
не будет надежно статически отправлен... это может произойти здесь, с анализом потока, поскольку все находится в одном TU, но вы можете создать unique_ptr‹Foo›, который содержит подтип Foo. Тот факт, что это unique_ptr‹Foo›, не дает никакой гарантии, что он неполиморфный, потому что в C++ нет синтаксиса для указания неполиморфного указателя на полиморфный тип. - person Stephen Lin   schedule 01.03.2013unique_ptr<foo_base>
будет два виртуальных звонка вместо одного. - person Stephen Lin   schedule 01.03.2013