Оптимизация вызова виртуальной функции

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

// Base.h
class Base
{
public:
    virtual void Foo() = 0;
};

// Concrete.h
class Concrete : public Base
{
public:
    virtual void Foo()
    {
        // do something;
    }
};

//Some.h
extern Base* const g_object;

// Some.cpp
Concrete on_stack_concrete;

Base* const g_object = &on_stack_concrete;

Предполагается, что хитрость заключается в использовании указателя const на переменную, которая размещается в стеке (не динамически), и компилятор наверняка ее оптимизирует. Таким образом, всякий раз, когда пользователь вызывает g_object->Foo(), часть //сделать что-то будет выполняться без необходимости поиска в v-таблице.

Это правда или нет?

Заранее спасибо за любой повтор.

РЕДАКТИРОВАТЬ:

Возможное использование такой конструкции заключается в ограничении интерфейса конкретных реализаций. Конечно, можно утверждать, что «ограниченные» методы должны быть закрытыми, но иногда другим модулям библиотеки требуется доступ к этим общедоступным дополнительным методам объекта, не позволяя пользователю манипулировать ими. Так, например, используя #define, можно создать код, похожий на:

// Some.cpp
#ifdef _WIN32
Win32Concrete concrete;
#elif defined _UNIX
UnixConcrete concrete;
#endif

Base* const g_global = &concrete;

На самом деле объявление этих классов может быть определено только в файле CPP, поэтому пользователь не знает об их существовании.

Вопрос не в том, зачем вообще использовать такой постоянный указатель, а в том, можно ли оптимизировать поиск в v-таблице в таком сценарии.


person Adrian Lis    schedule 22.09.2012    source источник
comment
Это, вероятно, зависит от компилятора, вы должны быть в состоянии проверить это, глядя на сборку, сгенерированную вашим компилятором.   -  person marcinj    schedule 22.09.2012
comment
Зачем использовать указатель, если он всегда указывает на один и тот же объект?   -  person Bo Persson    schedule 22.09.2012
comment
@BoPersson, прямо сейчас я могу придумать одно хорошее приложение, например кроссплатформенную библиотеку, которая предоставляет пользователям только абстрактные интерфейсы, а серверная часть использует несколько реализаций для разных платформ в зависимости от некоторых #define. Это, конечно, потребует многократной компиляции для каждой платформы с различным набором определений, но для конечного пользователя это не имеет значения.   -  person Adrian Lis    schedule 22.09.2012
comment
Если класс Win32Concrete не виден клиентам Some.h, то они вряд ли смогут оптимизировать виртуальную таблицу, поскольку они не могут видеть, что это такое на самом деле, поскольку вы не решаете это до момента компоновки. В любом случае оптимизация vtable не гарантируется, поэтому нет никакого способа заставить ее, кроме как отсутствием vtable.   -  person Raymond Chen    schedule 22.09.2012


Ответы (3)


Вы, кажется, неправильно используете virtual.

virtual реализует полиморфизм во время выполнения. И сценарий, который вы описываете, не использует и не нуждается в этом. Маловероятно, что и Win32Concrete, и UnixConcrete существуют в какой-либо среде компиляции.

Вместо:

// Some.cpp
#ifdef _WIN32
Win32Concrete concrete;
#elif defined _UNIX
UnixConcrete concrete;
#endif

Base* const g_global = &concrete;

использовать:

// CommonHeader.h
#ifdef _WIN32
typedef Win32Concrete Concrete;
#elif defined _UNIX
typedef UnixConcrete Concrete;
#endif

Теперь ваши функции не обязательно должны быть виртуальными.

person Drew Dormann    schedule 22.09.2012
comment
Смысл глобальной переменной, инициализированной таким образом, состоит в том, чтобы избежать передачи ее функциям в качестве параметров везде, где это необходимо, поэтому такого сценария просто не произойдет. Он глобальный, поэтому он доступен везде, поэтому контекст, как Вы упомянули, в моем понимании не меняется. - person Adrian Lis; 22.09.2012
comment
@AdrianLis, если вы напрямую обращаетесь к глобальной переменной, то зачем вообще создавать Base* const g_object? Вместо этого вы можете использовать on_stack_concrete.foo(). И в этом случае вы можете избежать поиска. - person Drew Dormann; 22.09.2012
comment
Потому что конкретный класс может предоставить пользователю другой (дополнительный) интерфейс, который он не сможет использовать напрямую. Вот почему указатель на базовый класс на месте, который ограничивает интерфейс только жизненно важными вещами. Я отредактирую вопрос, чтобы показать возможное использование. - person Adrian Lis; 22.09.2012
comment
Я знаю о таком решении, но Вы опять-таки упустили один важный момент внешнего воздействия. База — это общедоступный интерфейс для пользователей библиотеки. Конкретные реализации предназначены для деталей внутренней реализации библиотеки, и каждая из них может сильно различаться. Внутри, конечно, я использую конкретную переменную там, где требуется более богатый интерфейс, но пользователь должен получить унифицированный интерфейс независимо от того, что использует внутренняя реализация, и поскольку он не меняется во время выполнения и компилируется таким образом, вопрос в том, является ли поиск в v-таблице неизбежен. - person Adrian Lis; 22.09.2012
comment
Конечно, я мог бы использовать адаптер или оболочку для конкретных реализаций, которые ограничивали бы интерфейс, но это потребовало бы выполнения большого количества макросов препроцессора и дополнительного кода для каждой новой реализации. В этом контексте проще использовать полиморфизм времени выполнения, но возникают ненужные затраты на виртуальные функции. Вот почему я спросил, является ли решение, о котором я читал, жизнеспособным вариантом. - person Adrian Lis; 22.09.2012
comment
@AdrianLis Я бы подумал о том, почему вы хотите использовать виртуальные функции. Или задайте новый вопрос о том, как реализовать интерфейс времени компиляции без них. - person Drew Dormann; 22.09.2012

Этот подход используется в исходном коде Doom 3 (https://github.com/id-Software/DOOM-3-BFG/), например, neo/framework/FileSystem.h определяет это:

extern idFileSystem *       fileSystem;

И neo/framework/FileSystem.cpp определяет это:

idFileSystemLocal   fileSystemLocal;
idFileSystem *      fileSystem = &fileSystemLocal;

Единственное обсуждение, которое я смог найти по этому поводу, это: http://fabiensanglard.net/doom3/

Все объекты высокого уровня idTech4 — это абстрактные классы с виртуальными методами. Обычно это приводит к снижению производительности, поскольку каждый адрес виртуального метода необходимо искать в виртуальной таблице, прежде чем вызывать его во время выполнения. Но есть хитрость, чтобы этого избежать.

Поскольку объект, размещенный статически в сегменте данных, имеет известный тип, компилятор может оптимизировать поиск в виртуальной таблице при вызове методов commonLocal.

person Nico Zink    schedule 21.10.2016

Самый простой способ решить эту проблему — сделать классы, которым требуется доступ к ограниченному методу, друзьями класса Concrete. Тогда они получают полный доступ к классу, а все остальные получают только публичный доступ.

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

class Concrete
{
public:
    void foo() { ... }
protected:
    void bar() { ... }
};

class ConcretePrivate : public Concrete
{
public:
    void bar() { Concrete:: bar(); }
};

ConcretePrivate g_globalPrivate;
Concrete& g_global = g_globalPrivate;

Код, который использует g_global, может получить доступ только к конкретным методам. Код, использующий g_globalPrivate, может обращаться к методам ConcretePrivate.

person Raymond Chen    schedule 22.09.2012