2012-04-12 2 views
6

Этот вопрос касается не самого языка C++ (т. Е. Не о стандарте), а о том, как вызвать компилятор для реализации альтернативных схем для виртуальной функции.Альтернативные схемы для реализации vptr?

Общая схема реализации виртуальных функций - это указатель на таблицу указателей.

class Base { 
    private: 
     int m; 
    public: 
     virtual metha(); 
}; 

эквивалентно, скажем, C будет что-то вроде

struct Base { 
    void (**vtable)(); 
    int m; 
} 

первый член обычно является указателем на список виртуальных функций и т.д. (кусок области в памяти, приложение имеет без контроля). И в большинстве случаев это зависит от размера указателя перед рассмотрением членов и т. Д. Таким образом, в 32-битной схеме адресации около 4 байт и т. Д. Если вы создали список из 40k полиморфных объектов в ваших приложениях, это примерно 40k x 4 байта = 160 Кбайт перед любыми переменными-членами и т. Д. Я также знаю, что это самая быстрая и распространенная реализация среди компиляторов C++.

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

Альтернативный способ сделать то же самое, чтобы иметь первую переменную как идентификатор индекса к таблице vptrs (что то же самое в C, как показано ниже)

struct Base { 
    char classid;  // the classid here is an index into an array of vtables 
    int  m; 
} 

Если общее количество классов в приложения менее 255 (включая все возможные экземпляры шаблонов и т. д.), тогда символ достаточно хорош, чтобы удерживать индекс, тем самым уменьшая размер всех полиморфных классов в приложении (я исключаю проблемы выравнивания и т. д.).

Мои вопросы: есть ли какой-либо переключатель в GNU C++, LLVM или любой другой компилятор для этого? или уменьшить размер полиморфных объектов?

Редактировать: Я понимаю, что проблемы с выравниванием отмечены. Кроме того, если это было в 64-битной системе (предполагая 64-битный vptr), каждый член полиморфного объекта стоимостью около 8 байтов, то стоимость vptr составляет 50% от объема памяти. Это в основном относится к малым полиморфизмам, созданным в массе, поэтому мне интересно, возможна ли эта схема по крайней мере для определенных виртуальных объектов, если не для всего приложения.

+11

Элемент m, вероятно, все еще будет выровнен по границе DWORD, чтобы вы ничего не получили. – Henrik

+1

Проблема с этой схемой, вероятно, будет связана с выравниванием. Здесь структура Base в вашем втором примере не будет (обычно) занимать 5 байтов, сохраняя 3 байта на объект. В любом случае m будет выравниваться на границе 4 байта, поэтому у вас будет 3 байта впустую пространства –

+1

Я думаю, что этот метод реализации виртуальной таблицы почти универсален. Я, конечно, никогда не сталкивался с другим методом или с любыми вариантами в GCC или Visual C++, которые бы контролировали, как они были реализованы. –

ответ

2

Представляется интересным, но это не сработает, если исполняемый файл состоит из нескольких модулей, передающих объекты между ними. Если они скомпилированы отдельно (скажем, DLL), если один модуль создает объект и передает его другому, а другой вызывает виртуальный метод - как он узнает, к какой таблице относится classid? Вы не сможете добавить еще moduleid, потому что два модуля могут не знать друг о друге при компиляции. Таким образом, если вы не используете указатели, я думаю, что это тупик ...

+0

Согласен, но я думаю, что это частично будет касаться инструментальной цепочки, среды (динамической и т. Д.), ОС. – ManiP

+0

@ManiP, инструментальная цепочка может не знать, как были построены другие модули и что они содержат (это также может быть проблемой с обычными vptrs, которые не всегда размещаются в одном месте). Имейте в виду, что указатель на таблицу vptrs должен быть известен во время компиляции. Чтобы определить правильную таблицу во время выполнения, вам нужно знать происхождение объекта (какой модуль его создал), и это серьезные накладные расходы (вы не можете хранить эти данные в объекте во время компиляции). Поэтому, даже если вы каким-то образом устраните (часть) этого vptr, в итоге вы получите больше кода. – eran

+0

Хороший вопрос! Как и в Linux, вы не можете создать файл жесткой ссылки для файловой системы с файловой системой. Та же проблема связана с номером и разделами inode. –

2

Пар наблюдений:

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

  2. Стоимость хранения указателя была перенесена в код, где для каждого использования полиморфной функции требуется, чтобы код переводил идентификатор класса в указатель vtable или какую-либо эквивалентную структуру данных. Так что это не бесплатно. Очевидно, что компромисс стоимости зависит от объема кода и количества объектов.

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

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

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

я настоятельно рекомендую вам взглянуть на Ian Piumarta's Cola также на Wikipedia Cola

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

+0

Что касается пункта 2: стоимость виртуального вызова аналогична стоимости вызова через таблицу переходов (указателей функций). Что касается пункта 3: полезные, но тщательно упакованные объекты избегают этой проблемы. Что касается пункта 4: в худшем случае должно быть достаточно 32 бит. На 64-битной платформе это возможная экономия в 4 байта для каждого объекта. Спасибо за ссылку на COLA :) –

+0

О пункте 2: согласился, что это не бесплатно, и за дополнительную плату. Обычно это что-то вроде «classid x 4 + [base ptr to vtables]». Я считаю, что современные процессоры должны относительно быстро реализовать это. – ManiP

+0

@ManiP - Да. Стоимость - это дополнительные инструкции или два, потенциально в каждом месте в коде, который вызывает метод (очевидно, возможна некоторая оптимизация). – gbulmer

2

Короткий ответ: нет, я не знаю какого-либо переключателя для этого с любым распространенным компилятором C++.

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

Я также хотел бы отметить, что в целом это не принесло бы большого успеха. По крайней мере, в типичном случае вы хотите, чтобы каждый элемент в структуре/классе находился на «естественной» границе, что означает, что его начальный адрес кратен его размеру. Используя ваш пример класса, содержащего один int, компилятор выделил бы один байт для индекса vtable, а затем сразу три байта заполнения, чтобы следующий int поступил бы по адресу, который был кратным четырем. Конечным результатом будет то, что объекты класса будут занимать ровно столько же объема памяти, как если бы мы использовали указатель.

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

Чтобы получить от этого выгоду, вы должны знать об этом и иметь структуру с (например) тремя байтами данных, которые вы могли бы перемещать там, где захотите. Затем вы перенесите их, чтобы они были первыми элементами, явно определенными в структуре. К сожалению, это также означает, что если вы отключили этот переключатель, чтобы у вас был указатель vtable, вы попали бы в дополнение к вставке компилятора, которая в противном случае была бы лишней.

Подводя итог: это не реализовано, и если бы это было не так, как обычно, это не достигалось.

+0

* Конечным результатом было бы то, что объекты класса занимали бы точно такое же количество памяти, как если бы мы использовали указатель. * Только для 32-битных построений, для 64-битных построений указатель имеет 8 байтов, обычно в два раза больше целого числа. Но, конечно, любой другой указатель в классе привел бы к той же проблеме. –

+0

Я понимаю и соглашаюсь с вопросом о выравнивании. Да, большинство компиляторов заполнят эти дополнительные 3 символа, чтобы заполнить 32 бита. В 64-битных случаях накладные расходы кажутся двойными. Но мой вопрос заключается в том, поддерживают ли компиляторы/инструментальные средства, потому что в случае встроенных систем, где у вас есть полиморфные объекты, маленькие, но многие из которых пространство становится роскошью. – ManiP

+0

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

2

Нет, такого переключателя нет.

кодовой LLVM/Clang избегает виртуальных таблиц в классах, которые выделяются на десятках тысяч: это произведение хорошо в закрытой иерархии, поскольку один enum может перечислить все возможные классы, а затем каждый класс связан с значение enum. закрыт, очевидно, из-за enum.

Затем виртуальность реализуется с помощью switch на enum и соответствующего литья перед вызовом метода. Опять же, закрыто. switch должен быть изменен для каждого нового класса.


Первый вариант: внешний vpointer.

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

class Interface { 
public: 
    virtual ~Interface() {} 

    virtual Interface* clone() const = 0; // might be worth it 

    virtual void updateCount(int) = 0; 

protected: 
    Interface(Interface const&) {} 
    Interface& operator=(Interface const&) { return *this; } 
}; 

template <typename T> 
class InterfaceBridge: public Interface { 
public: 
    InterfaceBridge(T& t): t(t) {} 

    virtual InterfaceBridge* clone() const { return new InterfaceBridge(*this); } 

    virtual void updateCount(int i) { t.updateCount(i); } 

private: 
    T& t; // value or reference ? Choose... 
}; 

template <typename T> 
InterfaceBridge<T> interface(T& t) { return InterfaceBridge<T>(t); } 

Тогда, представляя себе простой класс:

class Counter { 
public: 
    int getCount() const { return c; } 
    void updateCount(int i) { c = i; } 
private: 
    int c; 
}; 

Вы можете хранить объекты в массиве:

static Counter array[5]; 

assert(sizeof(array) == sizeof(int)*5); // no v-pointer 

И все же использовать их с полиморфными функциями:

void five(Interface& i) { i.updateCount(5); } 

InterfaceBridge<Counter> ib(array[3]); // create *one* v-pointer 
five(ib); 

assert(array[3].getCount() == 5); 

Значение vs refe rence - это фактически проектная нагрузка. В общем, если вам нужно clone, вам нужно хранить по значению, и вам нужно клонировать, когда вы храните базовый класс (например, boost::ptr_vector). Можно реально обеспечить как интерфейсы (и мосты):

Interface <--- ClonableInterface 
    |     | 
InterfaceB  ClonableInterfaceB 

Это просто дополнительный набор текста.


Другое решение, гораздо более активное участие.

Коммутатор реализуется таблицей переходов. Такая таблица вполне может быть создан во время выполнения, в std::vector, например:

class Base { 
public: 
    ~Base() { VTables()[vpointer].dispose(*this); } 

    void updateCount(int i) { 
    VTables()[vpointer].updateCount(*this, i); 
    } 

protected: 
    struct VTable { 
    typedef void (*Dispose)(Base&); 
    typedef void (*UpdateCount)(Base&, int); 

    Dispose dispose; 
    UpdateCount updateCount; 
    }; 

    static void NoDispose(Base&) {} 

    static unsigned RegisterTable(VTable t) { 
    std::vector<VTable>& v = VTables(); 
    v.push_back(t); 
    return v.size() - 1; 
    } 

    explicit Base(unsigned id): vpointer(id) { 
    assert(id < VTables.size()); 
    } 

private: 
    // Implement in .cpp or pay the cost of weak symbols. 
    static std::vector<VTable> VTables() { static std::vector<VTable> VT; return VT; } 

    unsigned vpointer; 
}; 

а затем через Derived класс:

class Derived: public Base { 
public: 
    Derived(): Base(GetID()) {} 

private: 
    static void UpdateCount(Base& b, int i) { 
    static_cast<Derived&>(b).count = i; 
    } 

    static unsigned GetID() { 
    static unsigned ID = RegisterTable(VTable({&NoDispose, &UpdateCount})); 
    return ID; 
    } 

    unsigned count; 
}; 

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

О, и из-за выравнивания, как только класс Derived вводит указатель, существует риск того, что между Base и следующим атрибутом будет 4 байта заполнения. Вы можете использовать их, тщательно выбирая первые несколько атрибутов в Derived, чтобы избежать заполнения ...

+0

Не уверен, что вы подразумеваете под этим утверждением: «В базе данных LLVM/Clang избегаются виртуальные таблицы в классах, которые выделяются десятками тысяч». Как компилятор знает, какой тип классов выделяется десятками тысяч в приложении? Или я неправильно интерпретирую это? – ManiP

+0

@ManiP: Я имел в виду * codebase * очевидно. Во всяком случае, компилятор не делает, разработчики заботятся о том, чтобы не вводить виртуальные методы в эти иерархии и вместо этого прибегать к ручному переключению. –