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

reduce size of object (wasted) in Multi virtual inheritance

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

这是 MCVE 来演示问题 ( http://coliru.stacked-crooked.com/a/0509965bea19f8d9 )

#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 不能作为模板 class。这会对我造成循环依赖。
  2. 我必须能够从派生 class(如果有的话)调用基 class 的 函数 ,例如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>{},但不确定如何让它发挥作用。

让事情变得简单:

我正在使用 C++17。

编辑

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

编辑 3

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

真实情况下,恐怖钻石发生的情况很多class,这是最简单的情况。

目前,我的电话是这样的:-

 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+space HpOOFlyableOOBirdOO 智能感知- 导向风格。

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

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

这是在所有 classes 中维护虚拟的最紧凑的方法。

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

更小的 class 大小版本删除所有虚拟 class 东西:

#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();
    }
}

这个例子假设你的派生 classes 没有重叠的功能和不同的效果,如果你有那些你必须向你的基础 class 添加虚函数以获得 12 字节的额外开销(如果打包 class,则为 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 class 中,大小将保持不变。

结束编辑

虚拟继承只是在 class 的基础上增加一个指针大小,加上指针对齐(如果需要)。如果你真的需要一个虚拟的 class.

,你就无法解决这个问题

您应该问自己的问题是您是否真的需要它。根据您访问此数据的方法,情况可能并非如此。

考虑到您需要虚拟继承,但需要从所有 classes 调用所有常用方法,您可以拥有一个虚拟基础 class 并且使用比 space 少一点您的原始设计如下:

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 仍然有能力处理结果的情况下,您可以使用 compiler-specific 指令进一步减小大小,使数据结构尽可能小(下一个例子适用于 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 个字节(但如果内存是问题可能会有所帮助)。

此外,如果您确实为每个 classes 调用了一个函数,您应该只将它作为一个函数,在必要时覆盖它。

#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();
    }
};

这还可以让您避免在调用适当的函数之前弄清楚 class 哪个对象是什么。

此外,假设您最初的虚拟 class 继承想法最适合您的应用程序,您可以重组数据,以便更容易访问缓存目的,同时减小 classes 并同时访问您的函数:

#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++。

您可以使用以下技术避免虚拟继承:使除叶 classes 之外的所有 classes 完全抽象(无数据成员)。所有数据访问都是通过虚拟获取器进行的。

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

这样,您可以 non-vir 多次继承 class 而不会复制任何数据成员(因为首先有 none)。

在示例中 D 有两次 A,但这并不重要,因为 A 实际上 是空的。

对于典型的实现,您应该为每个最派生的 vptr class 加上每个基础的 vptr class 除了层次结构中每个级别的第一个。

当然,对于每个成员访问,您现在都有一个虚拟调用开销,但没有什么是免费的。

如果这个开销对您来说太多了,并且您仍然需要多态性,您可能需要以一种完全不涉及虚函数的 C++ 机制的方式来实现它。有很多方法可以做到这一点,但当然每种方法都有其自身的特殊缺点,因此很难推荐一种。