уменьшить размер объекта (впустую) в мульти виртуальном наследовании - PullRequest
1 голос
/ 10 июня 2019

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

Это MCVE для решения проблемы (http://coliru.stacked -crooked.com / a / 0509965bea19f8d9 )

enter image description here

#include<iostream>
class Base{
    public: int id=0;  
};
class B : public virtual Base{
    public: int fieldB=0;
    public: void bFunction(){
        //do something about "fieldB"     
    }
};
class C : public virtual B{
    public: int fieldC=0;
    public: void cFunction(){
        //do something about "fieldC"     
    }
};
class D : public virtual B{
    public: int fieldD=0;
};
class E : public virtual C, public virtual D{};
int main (){
    std::cout<<"Base="<<sizeof(Base)<<std::endl; //4
    std::cout<<"B="<<sizeof(B)<<std::endl;       //16
    std::cout<<"C="<<sizeof(C)<<std::endl;       //32
    std::cout<<"D="<<sizeof(D)<<std::endl;       //32
    std::cout<<"E="<<sizeof(E)<<std::endl;       //56
}

Я надеюсь, что sizeof(E) будет не более 16 байтов (id + fieldB + fieldC + fieldD).
Из эксперимента, если это не виртуальное наследование, размер E будет 24 ( MCVE ).

Как уменьшить размер E (с помощью магии C ++, изменить архитектуру программы или шаблон проектирования)?

Требование: -

  1. Base,B,C,D,E не может быть шаблоном класса. Это вызовет круговую зависимость для меня.
  2. Я должен иметь возможность вызывать функцию базового класса из производного класса (если есть), например. e->bFunction() и e->cFunction(), как обычно.
    Тем не менее, это нормально, если я не могу позвонить e->bField больше.
  3. Я все еще хочу легкость объявления.
    В настоящее время я могу легко объявить "E inherit from C and D" как class E : public virtual C, public virtual D.

Я думаю о CRTP, например class E: public SomeTool<E,C,D>{}, но не уверен, как заставить это работать.

Чтобы упростить задачу:

  • В моем случае каждый класс используется как монолитный, то есть я никогда не буду приводить объекты между типами, такими как static_cast<C*>(E*) или наоборот.
  • Макро разрешено, но не рекомендуется.
  • Допустимая идиома. На самом деле, ниже, что я мечтаю.
    Возможно, мне удастся удалить все виртуальное наследование.
    Однако, учитывая все требования, я не могу найти способ его кодировать.
    В pimpl, если я сделаю E виртуальное наследование от C & D и т. Д., Все вышеуказанные требования будут выполнены, но я все равно буду тратить много памяти. : -

enter image description here

Я использую C ++ 17.

Редактировать

Вот более правильное описание моей реальной проблемы.
Я создаю игру, которая имеет много компонентов, например B C D E.
Все они созданы через пул. Таким образом, он позволяет быстро итерировать.
В настоящее время, если я запрашиваю каждые E с игрового движка, я могу звонить e->bFunction().
В моем наиболее серьезном случае я трачу 104 байта на объект в E -подобном классе. (реальная иерархия более сложна)

enter image description here

Редактировать 3

Позвольте мне попробовать еще раз. Вот более значимая диаграмма классов.
У меня есть центральная система для автоматического назначения hpPtr, flyPtr, entityId, componentId, typeId.
не волнуйтесь, как они инициализируются.

enter image description here

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

В настоящее время я звоню как: -

 auto hps = getAllComponent<HpOO>();
 for(auto ele: hps){ ele->damage(); }
 auto birds = getAllComponent<BirdOO>();
 for(auto ele: birds ){ 
     if(ele->someFunction()){
          ele->suicidalFly();
          //.... some heavy AI algorithm, etc
     }
 }

При таком подходе я могу наслаждаться когерентностью кэша, как в Entity Component System, и классным ctrl+space intellisense HpOO, FlyableOO и BirdOO, как в объектно-ориентированном стиле.

Все отлично работает - просто слишком много памяти.

Ответы [ 2 ]

2 голосов
/ 10 июня 2019

РЕДАКТИРОВАТЬ: на основе последнего обновления вопроса и чата

Вот самое компактное ведение виртуального во всех ваших классах.

#include <iostream>
#include <vector>

using namespace std;

struct BaseFields {
    int entityId{};
    int16_t componentId{};
    int8_t typeId{};
    int16_t hpIdx;
    int16_t flyPowerIdx;
};

vector<int> hp; // this will contain all the hit points, dynamically resizable, logic up to you
vector<float> flyPower; // this will contain all the fly powers, dynamically resizable, logic up to you

class BaseComponent {
public: // or protected
    BaseFields data;
};
class HpOO : public virtual BaseComponent {
public:
    void damage() {
        hp[data.hpIdx] -= 1;
    }
};
class FlyableOO : public virtual BaseComponent {
public:
    void addFlyPower(float power) {
        flyPower[data.hpIdx] += power;
    }
};
class BirdOO : public virtual HpOO, public virtual FlyableOO {
public:
    void suicidalFly() {
        damage();
        addFlyPower(5);
    }
};

int main (){
    std::cout<<"Base="<<sizeof(BaseComponent)<<std::endl; // 12
    std::cout<<"C="<<sizeof(HpOO)<<std::endl; // 24
    std::cout<<"D="<<sizeof(FlyableOO)<<std::endl; // 24
    std::cout<<"E="<<sizeof(BirdOO)<<std::endl; // 32
}

версия меньшего размера класса, отбрасывающая все виртуальные вещи класса:

#include <iostream>
#include <vector>

using namespace std;

struct BaseFields {
};

vector<int> hp; // this will contain all the hit points, dynamically resizable, logic up to you
vector<float> flyPower; // this will contain all the fly powers, dynamically resizable, logic up to you

class BaseComponent {
public: // or protected
    int entityId{};
    int16_t componentId{};
    int8_t typeId{};
    int16_t hpIdx;
    int16_t flyPowerIdx;
protected:
    void damage() {
        hp[hpIdx] -= 1;
    };
    void addFlyPower(float power) {
        flyPower[hpIdx] += power;
    }
    void suicidalFly() {
        damage();
        addFlyPower(5);
    };
};
class HpOO : public BaseComponent {
public:
    using BaseComponent::damage;
};
class FlyableOO : public BaseComponent {
public:
    using BaseComponent::addFlyPower;
};
class BirdOO : public BaseComponent {
public:
    using BaseComponent::damage;
    using BaseComponent::addFlyPower;
    using BaseComponent::suicidalFly;
};

int main (){
    std::cout<<"Base="<<sizeof(BaseComponent)<<std::endl; // 12
    std::cout<<"C="<<sizeof(HpOO)<<std::endl; // 12
    std::cout<<"D="<<sizeof(FlyableOO)<<std::endl; // 12
    std::cout<<"E="<<sizeof(BirdOO)<<std::endl; // 12
    // accessing example
    constexpr int8_t BirdTypeId = 5;
    BaseComponent x;
    if( x.typeId == BirdTypeId ) {
        auto y = reinterpret_cast<BirdOO *>(&x);
        y->suicidalFly();
    }
}

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

и, возможно, самая маленькая версия, поддерживающая виртуалы

#include <iostream>
#include <vector>

using namespace std;

struct BaseFields {
    int entityId{};
    int16_t componentId{};
    int8_t typeId{};
    int16_t hpIdx;
    int16_t flyPowerIdx;
};

#define PACKED [[gnu::packed]]

vector<int> hp; // this will contain all the hit points, dynamically resizable, logic up to you
vector<float> flyPower; // this will contain all the fly powers, dynamically resizable, logic up to you

vector<BaseFields> baseFields;

class PACKED BaseComponent {
public: // or protected
    int16_t baseFieldIdx{};
};
class PACKED HpOO : public virtual BaseComponent {
public:
    void damage() {
        hp[baseFields[baseFieldIdx].hpIdx] -= 1;
    }
};
class PACKED FlyableOO : public virtual BaseComponent {
public:
    void addFlyPower(float power) {
        flyPower[baseFields[baseFieldIdx].hpIdx] += power;
    }
};
class PACKED BirdOO : public virtual HpOO, public virtual FlyableOO {
public:
    void suicidalFly() {
        damage();
        addFlyPower(5);
    }
};

int main (){
    std::cout<<"Base="<<sizeof(BaseComponent)<<std::endl; // 2
    std::cout<<"C="<<sizeof(HpOO)<<std::endl; // 16 or 10
    std::cout<<"D="<<sizeof(FlyableOO)<<std::endl; // 16 or 10
    std::cout<<"E="<<sizeof(BirdOO)<<std::endl; // 24 or 18
}

первое число для неупакованной структуры, второе упакованное

Вы также можете упаковать hpIdx и flyPowerIdx в entityId с помощью трюка объединения:

union {
    int32_t entityId{};
    struct {
    int16_t hpIdx;
    int16_t flyPowerIdx;
    };
};

в приведенном выше примере, если не использовать упаковку и переместить всю структуру BaseFields в класс BaseComponent, размеры остаются прежними.

END EDIT

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

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

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

class Base{
    public: int id=0;
    virtual ~Base();
    // virtual void Function();

};
class B : public  Base{
    public: int fieldB=0;
    // void Function() override;
};
class C : public  B{
    public: int fieldC=0;
};
class D : public  B{
    public: int fieldD=0;
};
class E : public  C, public  D{

};

int main (){
    std::cout<<"Base="<<sizeof(Base)<<std::endl; //16
    std::cout<<"B="<<sizeof(B)<<std::endl; // 16
    std::cout<<"C="<<sizeof(C)<<std::endl; // 24
    std::cout<<"D="<<sizeof(D)<<std::endl; // 24
    std::cout<<"E="<<sizeof(E)<<std::endl; // 48
}

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

#include<iostream>

class [[gnu::packed]] Base {
    public:
    int id=0;
    virtual ~Base();
    virtual void bFunction() { /* do nothing */ };
    virtual void cFunction() { /* do nothing */ }
};
class [[gnu::packed]] B : public Base{
    public: int fieldB=0;
    void bFunction() override { /* implementation */ }
};
class [[gnu::packed]] C : public B{
    public: int fieldC=0;
    void cFunction() override { /* implementation */ }
};
class [[gnu::packed]] D : public B{
    public: int fieldD=0;
};
class [[gnu::packed]] E : public C, public D{

};


int main (){
    std::cout<<"Base="<<sizeof(Base)<<std::endl; // 12
    std::cout<<"B="<<sizeof(B)<<std::endl; // 16
    std::cout<<"C="<<sizeof(C)<<std::endl; // 20
    std::cout<<"D="<<sizeof(D)<<std::endl; // 20
    std::cout<<"E="<<sizeof(E)<<std::endl; //40
}

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

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

#include<iostream>

class [[gnu::packed]] Base {
public:
    virtual ~Base();
    virtual void specificFunction() { /* implementation for Base class */ };
    int id=0;
};

class [[gnu::packed]] B : public Base{
public:
    void specificFunction() override { /* implementation for B class */ }
    int fieldB=0;
};

class [[gnu::packed]] C : public B{
public:
    void specificFunction() override { /* implementation for C class */ }
    int fieldC=0;
};

class [[gnu::packed]] D : public B{
public:
    void specificFunction() override { /* implementation for D class */ }
    int fieldD=0;
};

class [[gnu::packed]] E : public C, public D{
    void specificFunction() override {
        // implementation for E class, example:
        C::specificFunction();
        D::specificFunction();
    }
};

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

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

#include <iostream>
#include <array>

using namespace std;

struct BaseFields {
    int id{0};
};

struct BFields {
    int fieldB;
};

struct CFields {
    int fieldB;
};

struct DFields {
    int fieldB;
};

array<BaseFields, 1024> baseData;
array<BaseFields, 1024> bData;
array<BaseFields, 1024> cData;
array<BaseFields, 1024> dData;

struct indexes {
    uint16_t baseIndex; // index where data for Base class is stored in baseData array
    uint16_t bIndex; // index where data for B class is stored in bData array
    uint16_t cIndex;
    uint16_t dIndex;
};

class Base{
    indexes data;
};
class B : public virtual Base{
    public: void bFunction(){
        //do something about "fieldB"
    }
};
class C : public virtual B{
    public: void cFunction(){
        //do something about "fieldC"
    }
};
class D : public virtual B{
};
class E : public virtual C, public virtual D{};

int main (){
    std::cout<<"Base="<<sizeof(Base)<<std::endl; // 8
    std::cout<<"B="<<sizeof(B)<<std::endl; // 16
    std::cout<<"C="<<sizeof(C)<<std::endl; // 16
    std::cout<<"D="<<sizeof(D)<<std::endl; // 16
    std::cout<<"E="<<sizeof(E)<<std::endl; // 24
}

Очевидно, что это всего лишь пример, и предполагается, что у вас не более 1024 объектов в данный момент, вы можете увеличить это число, но выше 65536 вам придется использовать большее значение int для их хранения, также ниже 256 вы. можно использовать uint8_t для хранения индексов.

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

Получайте удовольствие и наслаждайтесь C ++.

1 голос
/ 11 июня 2019

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

class A {
 virtual int & a() = 0; // private!
 // methods that access a
};

class B : public A {
 virtual int & c() = 0; // private!
 // methods that access b
};

class C: public A {
 virtual int & c() = 0; // private!
 // methods that access c
};

class D: public B, public C {
 int & a() override { return a_; }
 int & b() override { return b_; } 
 int & c() override { return c_; }
 int a_, b_, c_; 
};

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

В примере Dимеет A дважды, но это не важно, поскольку A является практически пустым.

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

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

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

...