减少多重虚拟继承中对象(浪费)的大小

时间:2019-06-10 12:45:32

标签: c++ design-patterns multiple-inheritance virtual-inheritance pimpl-idiom

分析后,我发现程序的很大一部分内存被多虚拟继承所浪费。

这是 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习语。其实下面就是我的白日梦
    也许,我也许可以删除所有虚拟继承。
    但是,就所有要求而言,我找不到编码的方法。
    在pimpl中,如果我从E进行C & D虚拟继承,则可以满足上述所有要求,但是我仍然会浪费很多内存。 :-

enter image description here

我正在使用C ++ 17。

编辑

以下是对我的现实生活问题的更正确描述。
我创建了一个包含很多组件的游戏,例如B C D E
它们都是通过池创建的。因此,它可以实现快速迭代。
当前,如果我从游戏引擎中查询每个E,我将能够调用e->bFunction()
在最严重的情况下,我在类似E的类中为每个对象浪费了104个字节。 (实际的层次结构比较复杂)

enter image description here

编辑3

让我再试一次。这是一个更有意义的类图。
我已经有一个中央系统,可以自动分配hpPtrflyPtrentityIdcomponentIdtypeId
 即不用担心它们如何初始化。

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
     }
 }

通过这种方法,我可以像实体组件系统一样享受高速缓存的一致性,并且像面向对象一样享受ctrl+spaceHpOOFlyableOO的{​​{1}}智能感知样式。

一切正常-只是占用太多内存。

2 个答案:

答案 0 :(得分:2)

编辑:基于问题的最新更新和一些聊天

这是在您所有班级中维护虚拟的最紧凑的方式。

#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)。 / p>

还有可能是最小版本的虚拟机仍在维护中

#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类中,则大小保持不变。

结束编辑

虚拟继承只会将一个指针大小添加到类中,再加上指针的对齐方式(如果需要)。如果您确实需要虚拟课程,那么您将无法解决。

您应该问自己的问题是您是否真正需要它。可能不是这种情况,具体取决于您对这些数据的访问方法。

考虑到您需要虚拟继承,但是所有需要从所有类中调用的通用方法,您可以拥有一个虚拟基类,并且可以通过以下方式使用比原始设计少的空间:

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
}

在存在高速缓存未命中但CPU仍然有权处理结果的情况下,您可以使用特定于编译器的指令使数据结构尽可能小,以进一步减小大小(下一个示例在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
}

以可能的一些CPU开销为代价,额外节省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 :(得分:1)

您可以使用以下技术来避免虚拟继承:使除叶子类之外的所有类完全抽象(没有数据成员)。所有数据访问都是通过虚拟吸气剂进行的。

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 ++机制的方式来实现它。这样做的方法很多,但是当然每种方法都有其自身的特殊缺点,因此很难推荐一种。