Стоимость памяти таблицы виртуальных функций C ++ - PullRequest
12 голосов
/ 26 октября 2009

Рассмотрим:

class A
{
    public:
        virtual void update() = 0;
}

class B : public A
{
    public:
        void update() { /* stuff goes in here... */ }

    private:
        double a, b, c;
}

class C { 
  // Same kind of thing as B, but with different update function/data members
}

Я сейчас делаю:

A * array = new A[1000];
array[0] = new B();
array[1] = new C();
//etc., etc.

Если я вызываю sizeof(B), возвращаемый размер - это размер, требуемый 3 двойными членами, плюс некоторые накладные расходы, необходимые для таблицы указателей виртуальных функций. Теперь, возвращаясь к моему коду, получается, что sizeof (myclass) равен 32; то есть я использую 24 байта для своих элементов данных и 8 байтов для таблицы виртуальных функций (4 виртуальные функции). Мой вопрос: есть ли способ, как я могу упростить это? Моя программа в конечном итоге будет использовать чертовски много памяти, и мне не нравится, когда 25% ее потребляют указатели виртуальных функций.

Ответы [ 13 ]

42 голосов
/ 26 октября 2009

V-таблица для класса , а не для объекта. Каждый объект содержит только указатель на свою v-таблицу. Таким образом, издержки на экземпляр составляют sizeof(pointer) (обычно 4 или 8 байтов). Неважно, сколько у вас виртуальных функций для размера объекта класса. Учитывая это, я думаю, вы не должны слишком беспокоиться об этом.

11 голосов
/ 26 октября 2009

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

Нет никакого способа обойти это, но помните, что (опять же, как правило) каждая таблица виртуальных функций распределяется между всеми экземплярами класса, поэтому нет больших издержек на наличие нескольких виртуальных функций или дополнительных уровней наследования после того, как вы заплатили «налог vptr» (небольшая стоимость указателя vtable).

Для больших классов накладные расходы становятся намного меньше в процентах.

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

6 голосов
/ 26 октября 2009

Стоимость пространства vtable составляет один указатель (выравнивание по модулю). Сама таблица не помещается в каждый экземпляр класса.

6 голосов
/ 26 октября 2009

У вас есть два варианта.

1) Не беспокойся об этом.

2) Не используйте виртуальные функции. Однако отказ от использования виртуальных функций может просто переместить размер в ваш код, поскольку ваш код становится все более сложным.

5 голосов
/ 26 октября 2009

Отойдя от обычного указателя vtable в вашем объекте:

У вашего кода другие проблемы:

A * array = new A[1000];
array[0] = new B();
array[1] = new C();

Проблема, с которой вы столкнулись, - это проблема нарезки.
Вы не можете поместить объект класса B в пространство, зарезервированное для объекта класса A.
Вы просто отрежете часть B (или C) объекта, оставив только часть A.

Что вы хотите сделать. Имеет массив указателей A, чтобы он удерживал каждый элемент указателем.

A** array = new A*[1000];
array[0]  = new B();
array[1]  = new C();

Теперь у вас есть еще одна проблема разрушения. Хорошо. Это может продолжаться целую вечность.
Увеличение использования короткого ответа: ptr_vector <>

boost:ptr_vector<A>  array(1000);
array[0] = new B();
array[1] = new C();

Никогда не выделяйте массив таким образом, если только вам не нужно (это слишком Java, чтобы быть полезным).

2 голосов
/ 26 октября 2009

Если у вас будут миллионы таких вещей, а память вызывает у вас серьезную озабоченность, то вам, вероятно, не следует делать их объектами. Просто объявите их как структуру или массив из 3 двойных (или как угодно), и поместите функции для манипулирования данными в другом месте.

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

Вполне вероятно, что у вас будут большие группы объектов одного типа? В этом случае вы можете поместить информацию о типе на один уровень выше отдельных классов «А» ...

Что-то вроде:

class A_collection
{
    public:
        virtual void update() = 0;
}

class B_collection : public A_collection
{
    public:
        void update() { /* stuff goes in here... */ }

    private:
        vector<double[3]> points;
}

class C_collection { /* Same kind of thing as B_collection, but with different update function/data members */
2 голосов
/ 26 октября 2009

Сколько экземпляров классов, производных от A, вы ожидаете? Сколько разных A-производных классов вы ожидаете?

Обратите внимание, что даже с миллионом экземпляров речь идет о 32 МБ. До 10 миллионов, не парься.

Как правило, вам нужен дополнительный указатель на экземпляр (если вы работаете на 32-битной платформе, последние 4 байта должны быть выровнены). Каждый класс потребляет дополнительные (Number of virtual functions * sizeof(virtual function pointer) + fixed size) байтов для своего VMT.

Обратите внимание, что, учитывая выравнивание для двойников, даже один байт в качестве идентификатора типа увеличит размер элемента массива до 32. Так что решение Степана Раджко полезно в некоторых случаях, но не в вашем.

Кроме того, не забывайте накладные расходы на общую кучу для очень многих маленьких объектов. У вас может быть еще 8 байтов на объект. С помощью специального менеджера кучи - такого как объект / размер распределитель пула - вы можете сэкономить больше и использовать стандартное решение.

1 голос
/ 27 октября 2009

Как уже говорили другие, в типичном популярном подходе реализации, когда класс становится полиморфным, каждый экземпляр увеличивается на размер обычного указателя данных. Неважно, сколько виртуальных функций у вас есть в вашем классе. На 64-битной платформе размер увеличился бы на 8 байт. Если вы наблюдали 8-байтовый рост на 32-битной платформе, это могло быть вызвано добавлением отступа к 4-байтовому указателю для выравнивания (если ваш класс имеет требование 8-байтового выравнивания).

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

1 голос
/ 26 октября 2009

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

1 голос
/ 26 октября 2009

Вы добавляете один указатель на vtable для каждого объекта - если вы добавите несколько новых виртуальных функций, размер каждого объекта не увеличится. Обратите внимание, что даже если вы работаете на 32-битной платформе, где указатели имеют размер 4 байта, размер объекта увеличивается на 8, вероятно, из-за общих требований выравнивания структуры (т. Е. Вы получаете 4 байты заполнения).

Таким образом, даже если вы сделали класс не виртуальным, добавление одного члена char, скорее всего, добавит полные 8 байтов к размеру каждого объекта.

Я думаю, что единственным способом уменьшить размер ваших объектов будет:

  • сделать их не виртуальными (вам действительно нужно полиморфное поведение?)
  • используйте float вместо double для одного или нескольких элементов данных, если вам не нужна точность
  • если вы, вероятно, увидите много объектов с одинаковыми значениями для элементов данных, вы можете сэкономить пространство памяти в обмен на некоторую сложность в управлении объектами, используя шаблон проектирования Flyweight
...