Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Борьба с торможением виртуальных функций  (Прочитано 10375 раз)
0 Пользователей и 1 Гость смотрят эту тему.
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« : 30-08-2013 20:10 » 

Допустим, у вас есть класс, который имеет несколько вариантов реализации, и по какому-то условию инстанцироваться должна только одна из реализаций. Например, как в шаблоне проектирования "Стратегия".

Разумеется, стандартное ООП-решение
Код: (C++)
float s;

enum Types { Type1, Type2 };

class MyClass
{

  class Implementation
  {
  public:
    virtual void f() = 0;
    virtual ~Implementation() {}
  };

  class Implementation1 : public Implementation
  {
  public:
    virtual void f() { s += 1.0f; }
  };

  class Implementation2 : public Implementation
  {
  public:
    virtual void f() { s -= 1.0f; }
  };

  Implementation *implementation;

public:

  MyClass(const Types type) : implementation(NULL)
  {
    switch(type)
    {
    case Type1: implementation = new Implementation1(); break;
    case Type2: implementation = new Implementation2(); break;
    default: throw exception();
    }
  }

  void f()
  {
    implementation->f();
  }

  ~MyClass()
  {
    delete implementaiton;
  }

};
с реализацией интерфейса приводит к тому, что на каждый вызов MyClass::f приходится разыменовывать два указателя: сам объект implementation, и потом в нём виртуальную функцию. В результате, например, миллиард вызовов работает примерно 23-24 секунды вместо 4 секунд обычной функции:
Код: (C++)
void f()
{
  s += 1.0f;
}

Что же с этим делать?

Желание хотя бы заранее разыменовать указатель на implementation сопряжено с трудностями. Объект типа IImpl существовать не может - из-за абстрактного класса. Может существовать только ссылка, но её обязательно нужно инициализировать в конструкторе, да ещё и в списке инициализации. Поэтому можно соорудить конструктор с созданием объекта реализации при помощи вспомогательной статической функции-фабрики, а ссылку на этот объект инициализировать сразу после его создания:
Код: (C++)
class MyClass
{

  /* ... */

  Implementation *implementation;
  Implementation &reference;

  static Implementation *createImplementation(const Types type)
  {
    switch(type)
    {
    case Type1: return new Implementation1();
    case Type2: return new Implementation2();
    default: throw exception();
    }
  }
 
public:

  MyClass(const Types type) : implementation(createImplementation(type)), reference(*implementation)
  {}

  void f()
  {
    reference.f();
  }

  ~MyClass()
  {
    delete implementaiton;
  }

};
И надо заметить, это нам почти никак не помогло - основной вклад в падение производительности привносит именно вызов виртуальной функции.

Разные реализации находятся в разных классах. Свести типы этих классов к единому, чтобы назначить этот тип полю класса, можно только наследованием. Появившийся в C++11 вариант с auto применим лишь к локальным переменным внутри функций, но не к членам класса и не к возвращаемым значениям функций. Отказаться от виртуальных функций тоже нельзя - не будут вызываться методы реализаций. Вовсе отказаться от типизации сложно: во-первых, нетипизированным бывает лишь указатель, но не ссылка, во-вторых, для вызова метода всё равно нужно знать тип класса. Но по крайней мере такой вариант можно попробовать: оставить лишь указатель на реализацию, а выбор виртуальной функции заменить на жёсткое условие.
Код: (C++)
class MyClass
{

  class Implementation1
  {
  public:
    void f() { s += 1.0f; }
  };

  class Implementation2
  {
  public:
    void f() { s -= 1.0f; }
  };

  void *implementation;

public:

  MyClass(const Types type) : implementation(NULL)
  {
    switch(type)
    {
    case Type1: implementation = new Implementation1(); break;
    case Type2: implementation = new Implementation2(); break;
    default: throw exception();
    }
  }

  void f()
  {
    switch(type)
    {
    case Type1: static_cast<Implementation1 *>(implementation)->f(); break;
    case Type2: static_cast<Implementation2 *>(implementation)->f(); break;
    default: throw exception();
    }
  }

  ~MyClass()
  {
    delete implementaiton;
  }

};
И этот вариант работает лишь чуть медленнее простой функции f: чуть больше 4 секунд. Обработка жёсткого условия происходит гораздо быстрее выборки из таблицы виртуальных функций классов.

Последний вариант: использовать идею шаблона проектирования "Lightweight" для случая инициализации. Инстанцировать все варианты реализации сразу, но сделать их лёгкими объектами, отложив инициализацию до момента выбора рабочего варианта. Затем, как и в предыдущем случае, использовать жёсткое условие:
Код: (C++)
class MyClass
{

  class Implementation1
  {
  public:
    Implementation1() { /* Тут пусто */ }
    void init() { /* Тут основная инициализация */ }
    void f() { s += 1.0f; }
  };

  class Implementation2
  {
  public:
    Implementation2() { /* Тут пусто */ }
    void init() { /* Тут основная инициализация */ }
    void f() { s -= 1.0f; }
  };

  Implementation1 implementation;
  Implementation2 implementation;  

public:

  MyClass(const Types type)
  {
    /* Выбираем, какой объект будет рабочим, остальные остаются неинициализированными и "лёгкими". */
    switch(type)
    {
    case Type1: implementation1.init(); break;
    case Type2: implementation2.init(); break;
    default: throw exception();
    }
  }

  void f()
  {
    switch(type)
    {
    case Type1: implementation1.f(); break;
    case Type2: implementation2.f(); break;
    default: throw exception();
    }
  }

};
И этот вариант работает так же быстро, как простая функция - около 4 секунд.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Sla
Команда клуба

ua
Offline Offline
Пол: Мужской

WWW
« Ответ #1 : 30-08-2013 20:35 » 

м.... а почему не в статьях?
Записан

Мы все учились понемногу... Чему-нибудь и как-нибудь.
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #2 : 31-08-2013 07:08 » 

Sla, потому что я их не читаю и туда не хожу, а пишу себе на память - типа блога, и сразу видеть комментарии, если таковые воспоследуют. В поисковик это всё равно попадёт - кому надо, прочитают. А рубрикатор на форуме гораздо более ветвистый, чем в статьях - удобнее ориентироваться по тематике. И главное, на форуме есть свой поиск, а в статьях нету.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #3 : 31-08-2013 07:18 » 

Дим, а нет у вас там что-то типа указателей или ссылок на функции? Тогда инициализировать указатель в конструкторе и неудобоваримый switch превратится в одиночный if.

UPD: это спросонья подумал, что речь о C#... Улыбаюсь Указатель + инициализация его в конструкторе + пустой метод-затычка в базовом классе и безусловный вызов. Для удобства пользования обернуть методом.
« Последнее редактирование: 31-08-2013 07:28 от RXL » Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #4 : 31-08-2013 07:39 » 

А вот еще мысль (не проверял): если спецификация вызова у затычки и бекэнда совпадает, вызов может деградировать до goto. Если конечно компилятор не начудит.

Код: (C++)
class A {
    ... funcA(...) {}
  protected:
    ...(...)(*_func);
  public:
    ... func(...) { goto _func; }
    void A () : _func(funcA);
}
Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #5 : 31-08-2013 08:07 » 

Цитата: RXL
Тогда инициализировать указатель в конструкторе и неудобоваримый switch превратится в одиночный if.
Вот это принципиальный момент - указателя на функцию быть не должно. Как только он возникает где-нибудь, хоть прямо, хоть косвенно, так сразу просадка по производительности. Почему - не знаю. Феномен. Наблюдаю его как в VS2008, так и в VS2012. Приведение void * к типу указатель на класс исключает шаг разыменования указателя на функцию (метод), потому что компилятор сразу видит конечный тип, и в коде делает прямой вызов метода класса - профит в разы. switch или if существенного влияния на скорость работы не оказывают - в данном случае.

Разумеется, всё это актуально только для быстрых алгоритмов обработки больших массивов данных.

Что касается замены методов - мне это лично не подходит, мне нужны именно разные объекты реализаций (т.е. там внутри хранятся разные данные).
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines