Действительно ли можно ли кэшировать поиск виртуальной функции в C++?

Скажите, что у меня есть нечто вызова виртуальной функции () на указателе абстрактного базового класса, mypointer-> нечто (). Когда мое приложение запускает, на основе содержания файла, оно принимает решение инстанцировать конкретного реального класса и присваивает mypointer тому экземпляру. Для остальной части жизни приложения mypointer будет всегда указывать на объекты того конкретного типа. У меня нет способа знать то, что этот конкретный тип (он может инстанцировать фабрика в динамично загруженной библиотеке). Я только знаю, что тип останется таким же после первого раза сделан экземпляр конкретного типа. Указатель не может всегда указывать на тот же объект, но объект будет всегда иметь тот же конкретный тип. Заметьте, что тип технически определяется во 'времени выполнения', потому что это основано на содержании файла, но что после 'запуска' (файл загружается) тип фиксируется.

Однако в C++ я оплачиваю стоимость поиска виртуальной функции каждый раз, когда нечто называют на все время приложения. Компилятор не может оптимизировать взгляд далеко, потому что нет никакого пути к нему, чтобы знать, что конкретный тип не будет варьироваться во времени выполнения (даже если это был самый удивительный компилятор когда-нибудь, он не может размышлять о поведении динамично загруженных библиотек). На скомпилированном языке JIT как Java или.NET JIT может обнаружить, что тот же тип используется много раз, и действительно встройте cacheing. Я в основном ищу способ вручную сделать это для определенных указателей в C++.

Есть ли какой-либо путь в C++ для кэширования этого поиска? Я понимаю, что решениями мог бы быть симпатичный hackish. Я готов принять ABI/компилятор определенные взломы, если возможно записать, настраивают тесты, которые обнаруживают соответствующие аспекты ABI/компилятора так, чтобы это было "практически портативно" даже если не действительно портативный.

Обновление: голосующим против: Если это не стоило оптимизировать, то я сомневаюсь, что современные МОНЕТЫ В ПЯТЬ ЦЕНТОВ сделали бы это. Вы думаете, Sun и инженеры MS тратили впустую их время, реализовывая встроенный cacheing и не сравнивали его, чтобы гарантировать, что было улучшение?

35
задан Joseph Garvin 26 January 2010 в 19:31
поделиться

8 ответов

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

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

class MyConcrete : public MyBase
{
public:
  static void foo_nonvirtual(MyBase* obj);
  virtual void foo()
  { foo_nonvirtual(this); }
};

void (*f_ptr)(MyBase* obj) = &MyConcrete::foo_nonvirtual;
// Call f_ptr instead of obj->foo() in your code.
// Still not as good a solution as restructuring the algorithm.

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

2
ответ дан 27 November 2019 в 06:56
поделиться

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

  1. обеспечивают функцию как часть .dll, которая внутренне просто вызывает правильную функцию элемента напрямую. Вы оплачиваете стоимость косвенного прыжка, но, по крайней мере, вы не платите стоимость поиска VTBAL. Ваш пробег может варьироваться, но на определенных платформах вы можете оптимизировать косвенный вызов функции.
  2. Реструктурируйте ваше приложение, что вместо вызова функции участника в экземпляр, вы вызываете одну функцию, которая принимает коллекцию экземпляров. Mike Acton имеет замечательный пост (с определенной платформой и типом применения согнуты) по тому, почему и как вы должны это сделать.
4
ответ дан 27 November 2019 в 06:56
поделиться

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

1. Как мы можем сделать это быстро?

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

На самом деле, компиляторы, такие как PGO Visual C ++ (оптимизация профилирования с профилированием), имеют виртуальные спекуляции вызовов ( Ссылка ). Если результат профилирования может перечислять цели горячей виртуальной функции, то она переводится на прямой вызов , который может быть включен. Это также называется Deviortualization . Это также можно найти в некотором динамическом оптимизаторе Java.

2. Для тех, кто говорит, что это не нужно

, если вы используете языки сценариев, C # и беспокойство по поводу эффективности кодирования, да, это бесполезно. Однако любой, кто стремится сэкономить один цикл, чтобы получить лучшую производительность, то непрямая ветвь все еще важна проблема. Даже последние процессоры не очень хорошо обрабатывать виртуальные звонки. Один хороший пример будет виртуальной машиной или интерпретатором, который обычно имеет очень большой коммутатор. Его производительность в значительной степени связана с правильным прогнозированием косвенного ветви. Итак, вы не можете просто сказать, что это слишком низкий уровень или не нужно. Есть сотни людей, которые пытаются улучшить производительность внизу. Вот почему вы можете просто игнорировать такие детали :)

3. Некоторые скучные компьютерные архитектурные факты, связанные с виртуальными функциями

Dsimcha, написали хороший ответ на то, как CPU может эффективно обрабатывать виртуальный звонок. Но это не совсем правильно. Во-первых, все современные процессоры имеют предсказатель филиала, который буквально предсказывает результаты ветви для повышения пропускной способности трубопровода (или, более параллелизма на уровне инструкций, или ILP . Я даже могу сказать, что производительность CPU Очень в зависимости от того, насколько вы можете извлечь ILP из одного потока. Прогноз ветвления является наиболее важным фактором для получения более высокого ILP).

В предсказании ветвления есть два прогноза: (1) направление (т. Е. Филиал принимается? Или не принимается? Двоичный ответ), а (2) целевой целевой ответственность (то есть, куда я пойду? ). Основываясь на прогнозе, CPU умозрительно выполнить код. Если спекуляция не является правильным, то откаты CPU и перезапускаются от прогнозируемой ветви. Это полностью скрыто от вида программиста. Итак, вы на самом деле не знаете, что происходит внутри процессора, если вы не профилируете с vtune, который дает тарифы неправильной передачи.

В целом предсказание направления отделения очень точны (95% +), но все еще трудно предсказать целевые показатели, особенно виртуальные вызовы и коммутатор (I.E., Таблица прыжка). VRTUAL CALL - это косвенная ветвь , которая требует большему количеству нагрузки на память, а также CPU требует прогнозирования целевого действия ветви. Современные процессоры, такие как Nehalem and Nehalem и Phenom amd, имеют специализированную косвенную целевую таблицу ветви.

Тем не менее, я не думаю, что просматривает VTable, наносит много накладных расходов. Да, это требует большего количества нагрузки на память, которая может сделать кэш пропустить. Но после того, как VTable загружен в кэш, то он в значительной степени удара кэша. Если вы также обеспокоены этой стоимостью, вы можете произвести предварительный код для загрузки VTable заранее. Но реальная сложность вызова виртуальной функции заключается в том, что ЦП не может сделать большую работу для прогнозирования цели виртуального вызова, который может привести к частому слива трубопровода из-за неправильного назначения цели.

19
ответ дан 27 November 2019 в 06:56
поделиться

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

Местоподобный механизм метода может быть реализован более чем в одном из способов, но общее количество обнаружено, что запись в виртуальной таблице не является фактическим методом для вызова, а скорее какой-то код промежуточного «батута», вставленный компилятором, который Перемещает этот указатель до вызова фактического метода.

Когда отправка является самым простым, просто дополнительное повторное направление указателя, затем пытаясь оптимизировать его не имеет смысла. Когда проблема сложнее, то любое решение будет зависеть от компилятора и хакерашскими. Более того, вы даже не знаете в каком сценарии вы: если объекты загружены из DLL, то вы действительно не знаете, возвращается ли фактический экземпляр, принадлежит простую линейную иерархию наследования или более сложным сценарием.

4
ответ дан 27 November 2019 в 06:56
поделиться

Вы не можете использовать указатель метода, потому что указатели на функции членов не считаются ковариантными типами возврата. См. Пример ниже:

#include <iostream>

struct base;
struct der;

typedef void(base::*pt2base)();
typedef void(der::*pt2der)();

struct base {
    virtual pt2base method() = 0;
    virtual void testmethod() = 0;
    virtual ~base() {}
};

struct der : base {
    void testmethod() {
        std::cout << "Hello from der" << std::endl;
    }
    pt2der method() { **// this is invalid because pt2der isn't a covariant of pt2base**
        return &der::testmethod;
    }
};

Другой вариант будет иметь метод, заявленный метод PT2Base () , но затем возвращение будет недействительным, потому что FER :: TESTMethod не имеет значения PT2Base.

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

2
ответ дан 27 November 2019 в 06:56
поделиться

Вызов виртуальной функции обходится мне в два раза дороже: Стабильный поиск и вызов функции.

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

На самом деле я проверял это в прошлом, хотя и на языке программирования D, а не на Си++. Когда в настройках компилятора была отключена подстановка, и я вызывал одну и ту же функцию в цикле несколько миллионов раз, тайминги находились в пределах epsilon друг от друга, независимо от того, была ли эта функция виртуальной или нет.

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

37
ответ дан 27 November 2019 в 06:56
поделиться

Не могли бы вы использовать указатель метода?

Целью здесь заключается в том, что компилятор загрузит указатель на местоположение разрешенного метода или функции. Это произойдет однажды. После назначения код будет доступ к способу более непосредственно.

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

Я проверил сообщество Wiki, чтобы представить больше обсуждений.

1
ответ дан 27 November 2019 в 06:56
поделиться

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

Вот модель случая полиморфизма выполнения:

struct Base {
  virtual void doit(int&)=0;
};

struct Foo : public Base {
  virtual void doit(int& n) {--n;}
};

struct Bar : public Base {
  virtual void doit(int& n) {++n;}
};

void work(Base* it,int& n) {
  for (unsigned int i=0;i<4000000000u;i++) it->doit(n);
}

int main(int argc,char**) {
  int n=0;

  if (argc>1)
    work(new Foo,n);
  else
    work(new Bar,n);

  return n;
}

Это занимает ~ 14, чтобы выполнить в My Core2, скомпилированном с GCC 4.3.2 (32-битный Debian), -O3 .

Теперь предположим, что мы заменяем версию «Работа» с помощью шаблонной версии (на шаблонном виде на бетонном типе, над которым он будет работать):

template <typename T> void work(T* it,int& n) {
  for (unsigned int i=0;i<4000000000u;i++) it->T::doit(n);
}

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

Эй, Presto работает в 0,001 человека. Не плохо ускоряется фактор для двух линий! Тем не менее, обратите внимание, что массивная скорость вверх полностью связана с компилятором, после возможности полиморфизма времени выполнения в работе работают функции, просто оптимизируя петлю и компиляцию результата непосредственно в код. Но это на самом деле делает важным моментом: в моем опыте, основным природом от использования такого рода трюк приходит от возможностей для улучшения встроения и оптимизации, они позволяют компилятору, когда генерируется менее полиморфная, более конкретная функция, не Из простого удаления подражания на величину (которое действительно очень дешево).

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

Вы можете найти Этот этот связанный вопрос тоже интересно.

1
ответ дан 27 November 2019 в 06:56
поделиться
Другие вопросы по тегам:

Похожие вопросы: