Подтвердить что ты не робот

С++/компиляция: возможно ли установить размер vptr (глобальный индекс vtable + 2 байта)

Недавно я опубликовал вопрос об издержках памяти из-за виртуальности на С++. Ответы позволяют мне понять, как работает vtable и vptr. Моя проблема заключается в следующем: я работаю над суперкомпьютерами, у меня есть миллиарды некоторых объектов, и поэтому я должен заботиться о нехватке памяти из-за виртуальности. После некоторых мер, когда я использую классы с виртуальными функциями, каждый производный объект имеет свой 8-байтовый vptr. Это не является незначительным вообще.

Интересно, имеет ли intel icpc или g++ некоторую конфигурацию/параметр/параметры, чтобы использовать "глобальные" vtables и индексы с регулируемой точностью вместо vptr. Поскольку такая вещь позволила бы мне использовать 2-байтовый индекс (unsigned short int) вместо 8-байтных vptr для миллиардов объектов (и хорошее сокращение служебных данных памяти). Есть ли способ сделать это (или что-то в этом роде) с параметрами компиляции?

Большое спасибо.

4b9b3361

Ответ 1

К сожалению... не автоматически.

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

  • Внешний полиморфизм
  • Ручные v-таблицы
  • Ручной полиморфизм

1) Внешний полиморфизм

Идея состоит в том, что иногда вам требуется только полиморфизм в переходном режиме. То есть, например:

std::vector<Cat> cats;
std::vector<Dog> dogs;
std::vector<Ostrich> ostriches;

void dosomething(Animal const& a);

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

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

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

    virtual size_t age() const = 0;
    virtual size_t weight() const = 0;

    virtual void eat(Food const&) = 0;
    virtual void sleep() = 0;

private:
    Animal(Animal const&) = delete;
    Animal& operator=(Animal const&) = delete;
};

// Concrete class
class Cat {
public:
    size_t age() const;
    size_t weight() const;

    void eat(Food const&);
    void sleep(Duration);
};

Мост написан раз и навсегда:

template <typename T>
class AnimalT: public Animal {
public:
    AnimalT(T& r): _ref(r) {}

    virtual size_t age() const override { return _ref.age(); }
    virtual size_t weight() const { return _ref.weight(); }

    virtual void eat(Food const& f) override { _ref.eat(f); }
    virtual void sleep(Duration const d) override { _ref.sleep(d); }

private:
    T& _ref;
};

template <typename T>
AnimalT<T> iface_animal(T& r) { return AnimalT<T>(r); }

И вы можете использовать его так:

for (auto const& c: cats) { dosomething(iface_animal(c)); }

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

Альтернативой является AnimalT<T> работать со значениями (вместо ссылок) и предоставлять метод clone, который позволяет полностью выбирать между наличием v-указателя или нет в зависимости от ситуации.

В этом случае я советую использовать простой класс:

template <typename T> struct ref { ref(T& t): _ref(t); T& _ref; };

template <typename T>
T& deref(T& r) { return r; }

template <typename T>
T& deref(ref<T> const& r) { return r._ref; }

И затем немного измените мост:

template <typename T>
class AnimalT: public Animal {
public:
    AnimalT(T r): _r(r) {}

    std::unique_ptr< Animal<T> > clone() const { return { new Animal<T>(_r); } }

    virtual size_t age() const override { return deref(_r).age(); }
    virtual size_t weight() const { return deref(_r).weight(); }

    virtual void eat(Food const& f) override { deref(_r).eat(f); }
    virtual void sleep(Duration const d) override { deref(_r).sleep(d); }

private:
    T _r;
};

template <typename T>
AnimalT<T> iface_animal(T r) { return AnimalT<T>(r); }

template <typename T>
AnimalT<ref<T>> iface_animal_ref(T& r) { return Animal<ref<T>>(r); }

Этот путь вы выбираете, когда хотите полиморфное хранилище, а когда нет.


2) Ручные v-таблицы

(работает только на закрытых иератиях)

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

struct FooVTable {
    typedef void (Foo::*DoFunc)(int, int);

    DoFunc _do;
};

И затем укажите глобальный массив для иерархии, закрепленной в Foo:

extern FooVTable const* const FooVTableFoo;
extern FooVTable const* const FooVTableBar;

FooVTable const* const FooVTables[] = { FooVTableFoo, FooVTableBar };

enum class FooVTableIndex: unsigned short {
    Foo,
    Bar
};

Тогда все, что вам нужно в вашем классе Foo, должно удерживаться в самом производном типе:

class Foo {
public:

    void dofunc(int i, int j) {
        (this->*(table()->_do))(i, j);
    }

protected:
    FooVTable const* table() const { return FooVTables[_vindex]; }

private:
    FooVTableIndex _vindex;
};

Закрытая иерархия существует из-за массива FooVTables и перечисления FooVTableIndex, которые должны быть осведомлены обо всех типах иерархии.

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

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


3) Полиморфизм ручной работы

(работает только для закрытых иерархий)

Последний основан на моем опыте изучения кодовой базы LLVM/Clang. У компилятора есть та же самая проблема, с которой вы столкнулись: для десятков или сотен тысяч мелких предметов vpointer на элемент действительно увеличивает потребление памяти, что раздражает.

Поэтому они взяли простой подход:

  • у каждой иерархии классов есть компаньон enum, перечисляющий все члены
  • каждый класс в иерархии переводит свой компаньон enumerator к своей базе при построении
  • виртуальность достигается путем переключения enum и литья соответственно

В коде:

enum class FooType { Foo, Bar, Bor };

class Foo {
public:
    int dodispatcher() {
        switch(_type) {
        case FooType::Foo:
            return static_cast<Foo&>(*this).dosomething();

        case FooType::Bar:
            return static_cast<Bar&>(*this).dosomething();

        case FooType::Bor:
            return static_cast<Bor&>(*this).dosomething();
        }
        assert(0 && "Should never get there");
    }
private:
    FooType _type;
};

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

 // FooList.inc
 ACT_ON(Foo)
 ACT_ON(Bar)
 ACT_ON(Bor)

а затем выполните следующие действия:

 void Foo::dodispatcher() {
     switch(_type) {
 #   define ACT_ON(X) case FooType::X: return static_cast<X&>(*this).dosomething();

 #   include "FooList.inc"

 #   undef ACT_ON
     }

     assert(0 && "Should never get there");
 }

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

Очевидно, что единственным недостатком является то, что Foo.cpp должен включать все заголовки его производных классов. Это эффективно изолирует иерархию.


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

Одна важная вещь, в последних двух случаях уничтожение и копии требуют особого внимания.