...

суббота, 2 марта 2019 г.

[Из песочницы] novtable оптимизация


Компилятор Microsoft позволяет добавить расширение «novtable» для атрибута «__declspec» при объявлении класса.

Заявленная цель — значительно уменьшить размер генерируемого кода. На экспериментах с нашими компонентами уменьшение составило от 0,6 до 1,2 процента от размера DLL.

Применимость: классы, не предназначенные для создания экземпляров напрямую из них.

Например: чисто интерфейсные классы.

В коде это выглядит так:

struct __declspec(novtable) IDrawable
{
        virtual void Draw() const = 0;
};


Примечание: ключевое слово struct использовалось для декларации интерфейсного класса, чтобы избавить пример от не относящихся к теме статьи деталей; тогда как в случае использования class пришлось бы использовать public для указания «публичности» методов. По той же причине я не буду в этой статье добавлять виртуальный деструктор в интерфейсный класс.

Название «novtable» обещает, что виртуальной таблицы не будет… Но как же работает механизм вызова виртуальных функций в следующем коде:

// Добавим декларацию прямоугольника, реализующего интерфейс IDrawable:

class Rectangle : public IDrawable
{
        virtual void Draw() const override
        {
        }

        int width;
        int height;
};

…
IDrawable* drawable = new Rectangle;
drawable->Draw(); // происходит вызов Rectangle::Draw
…


Вспомним, что добавляется при объявлении виртуальной функции в классе:
  1. Определение таблицы виртуальных функций. Используется один экземпляр этой таблицы для всех экземпляров класса.
  2. В члены данных класса добавляется указатель на таблицу виртуальных функций.
  3. Код по инициализации этого указателя в конструкторе класса.

Таким образом, в нашем примере будет существовать декларация двух таблиц виртуальных функций: для IDrawable и для Rectangle. При создании объекта Rectangle первым выполняется конструктор IDrawable, который инициализирует указатель на свою таблицу виртуальных функций. Схематично это выглядит так:


Так как функция draw в IDrawable объявлена чисто-виртуальной (указано "=0" вместо тела функции), то в таблице виртуальных функций записан адрес генерируемой компилятором функции purecall.

Затем выполняется конструктор Rectangle, который инициализирует тот же указатель, но на свою таблицу виртуальных функций:


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

В этом случае при конструировании IDrawable указатель на таблицу виртуальных функций будет содержать непредсказуемое значение. Но это не должно нас беспокоить, так как создание реализации с обращением к виртуальным функциям до полного конструирования объекта, как правило, является ошибкой. Если, например, в конструкторе базового класса вызывать невиртуальную функцию этого класса, которая в свою очередь вызывает виртуальную функцию, то без novtable будет вызвана функция purecall, а с novtable — будет непредсказуемое поведение; ни один из вариантов не может быть приемлемым.

Заметим, что происходит не только уменьшение размера, но и некоторое ускорение работы программы.


Как известно, std::dynamic_cast позволяет приводить указатели и ссылки одного экземпляра класса к указателю и ссылке на другой, если эти классы связаны иерархией и являются полиморфными (содержат таблицу виртуальных функций). В свою очередь оператор typeid позволяет получать в runtime информацию об объекте по переданному ему указателю (ссылке) на этот объект. Эти возможности обеспечиваются механизмом RTTI, который использует информацию о типах, расположенную с привязкой к vtable класса. Детали структуры и расположения зависят от компилятора. В случае компилятора Microsoft схематично это выглядит так:

Поэтому если при сборке компилятору приказано включить RTTI, то novtable исключает еще и создание определения type_info для IDrawable и требуемых для нее служебных данных.
Заметим, что если у вас каким-то образом обеспечивается знание, что приводимый указатель (ссылка) на базовый класс указывает на реализацию производного, то std::static_cast эффективнее и не требует RTTI.


Помимо MSVC, данная возможность с тем же самым синтаксисом присутствует в Clang при компиляции под Windows.
  1. __declspec(novtable) — никак не влияет на объем памяти, занимаемый экземплярами класса.
  2. Уменьшение размера и некоторое ускорение работы программы обеспечивается за счет исключения определения неиспользуемых таблицы виртуальных функций, служебных данных RTTI и исключения кода инициализации указателя на таблицу виртуальных функций в конструкторах интерфейсных классов.

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий