Разве нельзя оптимизировать размер экземпляров класса с виртуальными методами во время выполнения с помощью g++?

Я только что проверил размер класса, содержащего десятки виртуальных методов, с помощью g++ (4.7), потому что я слышал, что указатели используются для виртуальных методов, и я подумал, что это будет ужасная реализация, так как она будет занимать 80 байт для каждого экземпляра класса. класс с всего 10 виртуальными методами в моей системе.

К моему облегчению, sizeof(<insert typename here>) вернул только 8 байтов, размер указателя в моей системе. Я предполагаю, что это означает, что он хранит указатель на vtable, а не на каждый метод, и что я просто неправильно понял, что говорили люди (или, возможно, большинство компиляторов глупы).

Однако, прежде чем я, наконец, протестировал это, я боролся с использованием виртуальных методов в качестве указателей так, как я ожидал, что они будут работать. Я заметил, что адрес на самом деле был относительно небольшим числом, часто меньше 100 и с разницей в 8 байт по сравнению с другими, поэтому я предположил, что это какой-то индекс для массива. И тут я задумался о том, как бы мне самому реализовать vtables, и чтобы не было использования указателя, как ясно показывают результаты моего теста. Я был удивлен, увидев, что он использует целых 8 байтов (я проверил, было ли это просто заполнение, вставив поле char, которое вернуло 16 байтов с sizeof).

Вместо этого я бы реализовал это, сохранив индекс массива (например, 4 байта или даже 2, если используется 65536 или меньше классов с виртуальными методами), который будет искаться в таблице поиска, содержащей указатели на виртуальные таблицы, и найти это так. Итак, почему указатель сохраняется? Из соображений производительности, или они просто повторно использовали код для 32-битных операционных систем (поскольку это не имело бы никакого значения в размере памяти)?

Заранее спасибо.

изменить:

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

#include <cstdio>
#include <cstdlib>

/* For the singleton lovers in this community */
class VirtualTableManager
{
    unsigned capacity, count;
    void*** vtables;
public:
    ~VirtualTableManager() {
        delete vtables;
    }
    static VirtualTableManager& getInstance() {
        static VirtualTableManager instance;
        return instance;
    }
    unsigned addElement(void** vtable) {
        if (count == capacity)
        {
            vtables = (void***) realloc(vtables, (capacity += 0x2000) * sizeof(void**));  /* Reserves an extra 64KiB of pointers */
        }
        vtables[count] = vtable;
        return count++;
    }
    void** getElement(unsigned index) {
        return index < capacity ? vtables[index] : 0; /* Just in case: "Hey guys, let's misuse the API!" */
    }
private:
    VirtualTableManager() : capacity(0), count(0), vtables(0) { }
    VirtualTableManager(const VirtualTableManager&);
    void operator =(const VirtualTableManager&);
};

class Real
{
public:
    short someField; /* This is required to show the difference, because of padding */
    Real() : someField(0) { }
    virtual ~Real() {
        printf("Real::~Real()\n");
    }
    virtual void method0() {
        printf("Real::method0()\n");
    }
    virtual void method1(short argument) {
        someField = argument;
    }
    virtual short method2() {
        return someField;
    }
    virtual void method3() { }
    virtual void method4() { }
    virtual void method5() { }
    virtual void method6() { }
    virtual void method7() { }
    virtual void method8() { }
};

class Fake
{
    static void** vtable;
    static unsigned classVIndex; /* Don't know what to call it, please forgive me for the lame identifier */
public:
    unsigned instanceVIndex;
    short someField;
    Fake() : instanceVIndex(classVIndex), someField(0) { }
    ~Fake() {
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[9])(this);
    }
    void method0() {
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[0])(this);
    }
    void method1(short argument) {
        reinterpret_cast<void (*)(Fake*, short argument)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[1])(this, argument);
    }
    short method2() {
        return reinterpret_cast<short (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[2])(this);
    }
    void method3() {
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[3])(this);
    }
    void method4() {
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[4])(this);
    }
    void method5() {
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[5])(this);
    }
    void method6() {
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[6])(this);
    }
    void method7() {
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[7])(this);
    }
    void method8() {
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[8])(this);
    }
protected:
    Fake(unsigned instanceVIndex, short someField)
        : instanceVIndex(instanceVIndex), someField(someField) { }
    /* The 'this' keyword is an automatically passed pointer, so I'll just manually pass it and identify it as 'self' (thank you, lua, I would have used something like 'vthis', which would be boring and probably incorrect) */
    static void vmethod0(Fake* self) {
        printf("Fake::vmethod0(%p)\n", self);
    }
    static void vmethod1(Fake* self, short argument) {
        self->someField = argument;
    }
    static short vmethod2(Fake* self) {
        return self->someField;
    }
    static void vmethod3(Fake* self) { }
    static void vmethod4(Fake* self) { }
    static void vmethod5(Fake* self) { }
    static void vmethod6(Fake* self) { }
    static void vmethod7(Fake* self) { }
    static void vmethod8(Fake* self) { }
    static void vdestructor(Fake* self) {
        printf("Fake::vdestructor(%p)\n", self);
    }
};

class DerivedFake : public Fake
{
    static void** vtable;
    static unsigned classVIndex;
public:
    DerivedFake() : Fake(classVIndex, 0) { }
    ~DerivedFake() {
        reinterpret_cast<void (*)(DerivedFake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[1])(this);
    }
    void method0() {
        reinterpret_cast<void (*)(DerivedFake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[0])(this);
    }
protected:
    DerivedFake(unsigned instanceVIndex, short someField)
        : Fake(instanceVIndex, someField) { }
    static void vmethod0(DerivedFake* self) {
        printf("DerivedFake::vmethod0(%p)\n", self);
    }
    static void vdestructor(DerivedFake* self) {
        printf("DerivedFake::vdestructor(%p)\n", self);
        Fake::vdestructor(self); /* call parent destructor */
    }
};

/* Make the vtable */
void** Fake::vtable = (void*[]) {
    (void*) &Fake::vmethod0, (void*) &Fake::vmethod1,
    (void*) &Fake::vmethod2, (void*) &Fake::vmethod3,
    (void*) &Fake::vmethod4, (void*) &Fake::vmethod5,
    (void*) &Fake::vmethod6, (void*) &Fake::vmethod7,
    (void*) &Fake::vmethod8, (void*) &Fake::vdestructor
};
/* Store the vtable and get the look-up index */
unsigned Fake::classVIndex = VirtualTableManager::getInstance().addElement(Fake::vtable);

/* Do the same for derived class */
void** DerivedFake::vtable = (void*[]) {
    (void*) &DerivedFake::vmethod0, (void*) &Fake::vmethod1,
    (void*) &Fake::vmethod2, (void*) &Fake::vmethod3,
    (void*) &Fake::vmethod4, (void*) &Fake::vmethod5,
    (void*) &Fake::vmethod6, (void*) &Fake::vmethod7,
    (void*) &Fake::vmethod8, (void*) &DerivedFake::vdestructor
};
unsigned DerivedFake::classVIndex = VirtualTableManager::getInstance().addElement(DerivedFake::vtable);

int main_virtual(int argc, char** argv)
{
    printf("size of 100 instances of Real including padding is %lu bytes\n"
           "size of 100 instances of Fake including padding is %lu bytes\n",
            sizeof(Real[100]), sizeof(Fake[100]));
    Real *real = new Real;
    Fake *fake = new Fake;
    Fake *derived = new DerivedFake;
    real->method1(123);
    fake->method1(456);
    derived->method1(789);
    printf("real::method2() = %hi\n"
           "fake::method2() = %hi\n"
           "derived::method2() = %hi\n", real->method2(), fake->method2(), derived->method2());
    real->method0();
    fake->method0();
    derived->method0();
    delete real;
    delete fake;
    delete derived;
    return 0;
}

Не бойтесь, я обычно не даю определение в таких классах. Я просто сделал это здесь, чтобы надеюсь улучшить читабельность. Во всяком случае, вывод:

size of 100 instances of Real including padding is 1600 bytes
size of 100 instances of Fake including padding is 800 bytes
real::method2() = 123
fake::method2() = 456
derived::method2() = 789
Real::method0()
Fake::vmethod0(0x1bd8040)
DerivedFake::vmethod0(0x1bd8060)
Real::~Real()
Fake::vdestructor(0x1bd8040)
DerivedFake::vdestructor(0x1bd8060)
Fake::vdestructor(0x1bd8060)

Возможно, он не является потокобезопасным, может содержать устрашающий легион ошибок, а также может быть относительно неэффективным, но я надеюсь, что он демонстрирует мою концепцию. Он был протестирован на 64-битной Ubuntu с g++-4.7. Я сомневаюсь, что в 32-битных системах есть какое-либо преимущество в размере, и, поскольку я сохраняю меньше слова (4 байта, так много для этого!), Мне пришлось поместить туда поле, чтобы показать эффекты. Не стесняйтесь тестировать скорость (пожалуйста, сначала оптимизируйте ее, если вы это сделаете, я поторопился с этим) или протестируйте эффекты на других архитектурах / платформах и с другими компиляторами (я хотел бы увидеть результаты, поэтому, пожалуйста, поделитесь ими, если вы это сделаете) ). Нечто подобное может быть полезно, когда кто-то находит необходимость создать 128/256-битную платформу, создает процессор с очень ограниченной поддержкой памяти, но невероятной скоростью или с компиляторами, которые используют около 21 байта для виртуальной таблицы на каждом экземпляре.

изменить:

Упс, пример кода был сумасшедший. Починил это.


person RPFeltz    schedule 26.07.2012    source источник
comment
Это может создать странные побочные эффекты в коде, с которыми может быть трудно справиться. Например, смещение члена данных в классе изменяет что-то еще, потому что был добавлен новый виртуальный класс. Кроме того, пространство обменивается на время, потому что вызов виртуального метода теперь требует двух различий.   -  person jxh    schedule 26.07.2012
comment
@ user315052- Разве уже не требуются два разыменования - одно для указателя vtable и одно для начального адреса функции?   -  person templatetypedef    schedule 26.07.2012
comment
Это сделало бы одно дополнительное разыменование.   -  person Puppy    schedule 26.07.2012
comment
@RPFeltz Я вижу, что вы уже приняли ответ, но не могли бы вы добавить к своему вопросу расчет того, сколько памяти вы можете сэкономить. Например, у вас есть приложение со 100 классами, каждый из которых имеет по 10 виртуальных методов. В случае использования vtables ваше приложение будет использовать 100 * 10 * sizeof (указатель функции) байт для хранения vtables. И сколько памяти вы сможете сэкономить, если ваша идея будет использована?   -  person    schedule 26.07.2012
comment
@skwllsp Я имел в виду способ сопоставления классов с правильными vtables, а не сохранение фактических указателей функций (которые, как я раньше полагал, хранились в реальном экземпляре). В настоящее время кажется, что это указатель на таблицу, содержащую указатели на функции, и я предлагал способ еще больше уменьшить размер, используя индекс для таблицы, содержащей указатели на таблицы, содержащие указатели на функции. Хотите пример кода того, как это сэкономит память при создании экземпляров?   -  person RPFeltz    schedule 26.07.2012
comment
@RPFeltz Да, я понимаю, что вы предлагаете - an index for a table containing pointers to tables containing pointers to functions. Мне действительно интересно понять, сколько памяти в простом сканарио (100 классов с 10 виртуальными функциями в каждом) точно будет использовать ваша реализация.   -  person    schedule 26.07.2012
comment
Не проще ли было бы использовать усеченные указатели и поместить все виртуальные таблицы ниже отметки 4 ГБ?   -  person harold    schedule 26.07.2012
comment
@harold Можете ли вы обрезать указатели, если размер vtable является переменным? Или что вы имели в виду под усечением? А что с динамической линковкой, стоит ли резервировать 4гб в каждом исполняемом файле для vtables? Я сомневаюсь в этом.   -  person RPFeltz    schedule 26.07.2012
comment
Ну, вам нужно только убедиться, что старшие 32 бита точки всегда равны нулю, и тогда вы можете просто забыть об этих старших битах. На самом деле это не подразумевает резервирование 4 ГБ для виртуальных таблиц, это просто означает, что вы размещаете виртуальные таблицы ниже 4 ГБ, возможно, вместе с тем, что вы хотите туда поместить, или просто пробел (незарезервированные страницы), который ничего не стоит.   -  person harold    schedule 27.07.2012
comment
@harold О, вы имели в виду целое число из 32 бит под усеченным указателем? Это интересная идея. Также предполагается, что вы не используете более 4 ГБ в загруженных библиотеках вместе взятых (у меня все равно только 4 ГБ ОЗУ). Но что бы вы сделали с библиотеками, загружаемыми во время выполнения, если это действительно возможно? Представьте, что вы резервируете часть памяти для виртуальных таблиц, чтобы их можно было загрузить туда. И вдруг вы загружаете libbehemoth, короля раздутых библиотек. Вы не зарезервировали достаточно памяти для загрузки виртуальных таблиц. Что делать? 1: (Ядро) паника. 2: Сдвиньте всю память, чтобы освободить место. 3: Ты скажи мне.   -  person RPFeltz    schedule 27.07.2012
comment
Если только популярные операционные системы не позволяют загружать указатели относительно начала вашей программы в памяти. Это сделало бы это довольно простым. Как он вообще справляется с такими вещами?   -  person RPFeltz    schedule 27.07.2012
comment
С тем, что я имел в виду, вероятно, хороший сбой.. Я, вероятно, должен был сначала подумать об этом :)   -  person harold    schedule 27.07.2012


Ответы (3)


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

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

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

Наконец, есть проблемы с выравниванием. Использование двух или четырех байтов для индекса, когда размер слова равен восьми байтам, может ухудшить производительность программы, если это сместит все остальные поля объекта. На самом деле вполне возможно, что g++ использует только четыре байта, а затем дополняет до восьми.

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

Надеюсь это поможет!

person templatetypedef    schedule 26.07.2012
comment
Спасибо за этот отличный и наиболее удовлетворительный ответ! Я уже сомневался в размерах переменных индексов, но 4 байта было бы достаточно для работы, и все же это был бы небольшой бонус. Тем не менее, проблема динамического связывания, безусловно, является допустимым моментом, о котором я не думал, и я считаю, что это делает весь этот метод бессмысленным, поскольку он потребует дополнительной информации (если вы не объедините таблицы где-нибудь и не пожертвуете большей производительностью, сделав позицию массива указатель). Я также рассматривал возможность бонуса производительности от использования размера слова, и это подтверждает это. - person RPFeltz; 26.07.2012

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

Если вы поместите в класс 2- или 4-байтовый индекс, а затем я добавлю указатель в качестве первого члена, потребуется некоторое дополнение, чтобы получить правильное выравнивание для моего указателя.

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

Я могу согласиться с тем, что это не всегда уменьшение размера, но я не хочу терять скорость из-за отсутствия увеличения размера.

person Bo Persson    schedule 26.07.2012

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

person Puppy    schedule 26.07.2012