gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-30 02:15:11 25 4
gpt4 key购买 nike

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

这是 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

让我再试一次。下面是一个更有意义的类图。
我有一个中央系统可以分配 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
}
}

通过这种方法,我可以享受实体组件系统中的缓存一致性,以及很酷的 ctrl+space HpOO 的智能感知, FlyableOOBirdOO就像面向对象的风格。

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

最佳答案

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

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

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

这个例子假设你的派生类没有重叠的功能和不同的效果,如果你有那些你必须向你的基类添加虚函数以获得 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
}

第一个数字是未打包的结构,第二个是打包的

您还可以使用 union 技巧将 hpIdx 和 flyPowerIdx 打包到 entityId 中:

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

在上面的示例中,如果不使用打包并将整个 BaseFields 结构移动到 BaseComponent 类中,则大小保持不变。

结束编辑

虚拟继承只是为类增加一个指针大小,加上指针对齐(如果需要)。如果您确实需要虚拟类(class),就无法解决这个问题。

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

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

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

关于c++ - 减少多虚拟继承中对象的大小(浪费),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56526705/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com