C ++虚函数表内存开销

时间:2009-10-26 17:55:03

标签: c++ memory virtual

考虑:

class A
{
    public:
        virtual void update() = 0;
}

class B : public A
{
    public:
        void update() { /* stuff goes in here... */ }

    private:
        double a, b, c;
}

class C { 
  // Same kind of thing as B, but with different update function/data members
}

我现在正在做:

A * array = new A[1000];
array[0] = new B();
array[1] = new C();
//etc., etc.

如果我调用sizeof(B),返回的大小是3个双成员所需的大小,加上虚函数指针表所需的一些开销。现在,回到我的代码,结果是'sizeof(myclass)'是32;也就是说,我的数据成员使用24个字节,虚拟功能表使用8个字节(4个虚函数)。我的问题是:有什么办法可以简化这个吗?我的程序最终会使用大量的内存,我不喜欢它的25%被虚拟函数指针吃掉的声音。

13 个答案:

答案 0 :(得分:42)

v-table是每个类而不是每个对象。每个对象只包含其v-table的指针。因此每个实例的开销是sizeof(pointer)(通常是4或8个字节)。对于类对象的大小,您有多少虚函数无关紧要。考虑到这一点,我认为你不应该过分担心它。

答案 1 :(得分:11)

通常,具有至少一个虚函数的类的每个实例都将使用其显式数据成员存储一个额外的指针。

没有办法解决这个问题,但请记住(通常也是)每个虚拟函数表在类的所有实例之间共享,因此在付款后拥有多个虚拟函数或额外级别的继承没有很大的开销“vptr tax”(vtable指针的小成本)。

对于较大的类,开销会以百分比变小。

如果您想要的功能与虚拟功能的功能类似,那么您将不得不以某种方式为其付费。实际上使用原生虚拟功能可能是最便宜的选择。

答案 2 :(得分:6)

您有两种选择。

1)不要担心。

2)不要使用虚拟功能。但是,不使用虚函数只需将大小移动到代码中,因为代码变得更复杂。

答案 3 :(得分:6)

vtable的空间成本是一个指针(模对齐)。表本身不会放在类的每个实例中。

答案 4 :(得分:5)

远离对象中vtable指针的非问题:

您的代码还有其他问题:

A * array = new A[1000];
array[0] = new B();
array[1] = new C();

你遇到的问题是切片问题 您不能将B类对象放入为A类对象保留的大小的空间中。
你只需将对象的B(或C)部分切掉,然后只留下A部分。

你想做什么。有一个A指针数组,以便它通过指针保存每个项目。

A** array = new A*[1000];
array[0]  = new B();
array[1]  = new C();

现在你有另一个破坏问题。好。这可能会持续很长时间 简答题使用boost:ptr_vector<>

boost:ptr_vector<A>  array(1000);
array[0] = new B();
array[1] = new C();

永远不要像这样分配数组,除非你必须(它太像Java一样有用)。

答案 5 :(得分:2)

您期望有多少个A派生类的实例? 你期望有多少个不同的A派生类?

请注意,即使有数百万个实例,我们所说的总共32MB。高达1000万,不要出汗。

通常每个实例需要一个额外的指针(如果你在32位平台上运行,最后4个字节是由于对齐)。每个类为其VMT消耗额外的(Number of virtual functions * sizeof(virtual function pointer) + fixed size)个字节。

请注意,考虑到双精度的对齐,即使单个字节作为类型标识符,也会将数组元素大小调整为32.因此,Stjepan Rajko的解决方案在某些情况下很有用,但不适用于您的解决方案。

另外,不要忘记这么多小对象的常规堆的开销。每个对象可能有另外8个字节。使用自定义堆管理器 - 例如特定于对象/大小的pool allocator - 您可以在此处节省更多并使用标准解决方案。

答案 6 :(得分:2)

如果你将拥有数以百万计的这些东西,并且记忆是你的一个严重问题,那么你可能不应该让它们成为对象。只需将它们声明为结构或3个双精度(或其他)的数组,并将函数放在其他地方操作数据。

如果你真的需要多态行为,你可能无法获胜,因为你必须存储在你的结构中的类型信息最终会占用相似的空间......

您是否可能拥有所有相同类型的大型对象组?在这种情况下,您可以将类型信息从单个“A”类中“提升”一级...

类似的东西:

class A_collection
{
    public:
        virtual void update() = 0;
}

class B_collection : public A_collection
{
    public:
        void update() { /* stuff goes in here... */ }

    private:
        vector<double[3]> points;
}

class C_collection { /* Same kind of thing as B_collection, but with different update function/data members */

答案 7 :(得分:1)

如果您事先知道所有派生类型及其各自的更新函数,则可以将派生类型存储在A中,并为更新方法实现手动分派。

然而,正如其他人指出的那样,你真的没有为vtable支付那么多钱,而且权衡是代码的复杂性(根据对齐情况,你可能根本不会节省任何内存!)。此外,如果您的任何数据成员都有析构函数,那么您还必须担心手动调度析构函数。

如果你仍想走这条路,它可能会是这样的:

class A;
void dispatch_update(A &);

class A
{
public:
    A(char derived_type)
      : m_derived_type(derived_type)
    {}
    void update()
    {
        dispatch_update(*this);
    }
    friend void dispatch_update(A &);
private:
    char m_derived_type;
};

class B : public A
{
public:
    B()
      : A('B')
    {}
    void update() { /* stuff goes in here... */ }

private:
    double a, b, c;
};

void dispatch_update(A &a)
{
    switch (a.m_derived_type)
    {
    case 'B':
        static_cast<B &> (a).update();
        break;
    // ...
    }
}

答案 8 :(得分:1)

您正在向每个对象添加一个指向vtable的指针 - 如果添加几个新的虚函数,则每个对象的大小不会增加。请注意,即使您使用的是指针为4字节的32位平台,您也会看到对象的大小增加8,这可能是由于结构的整体对齐要求(即,您得到4填充字节。)

因此,即使您使该类非虚拟,添加单个char成员也可能会为每个对象的大小添加完整的8个字节。

我认为你能够减少对象大小的唯一方法是:

  • 让它们非虚拟(你真的需要多态行为吗?)
  • 如果您不需要精度
  • ,请为一个或多个数据成员使用浮点数而不是double
  • 如果您可能会看到许多具有相同数据成员值的对象,您可以节省内存空间,以换取使用Flyweight design pattern

答案 9 :(得分:1)

不直接回答问题,但也要考虑数据成员的声明顺序可以增加或减少每个类对象的实际内存消耗。这是因为大多数编译器都不能(读取:不)优化类成员在内存中布局的顺序,以减少因对齐问题导致的内部碎片。

答案 10 :(得分:1)

正如其他人已经说过的,在一种典型的流行实现方法中,一旦一个类变成多态,每个实例都会增长一个普通数据指针的大小。你班上有多少虚拟功能并不重要。在64位平台上,大小将增加8个字节。如果您在32位平台上观察到8字节增长,则可能是由于填充添加到4字节指针进行对齐(如果您的类具有8字节对齐要求)。

此外,值得注意的是虚拟继承可以将额外的数据指针注入到类实例(虚拟基指针)中。我只熟悉一些实现,并且至少有一个虚拟基本指针的数量与类中的虚拟基数相同,这意味着虚拟继承可能会为每个实例添加多个内部数据指针。

答案 11 :(得分:0)

考虑到已经存在的所有答案,我认为我一定很疯狂,但这对我来说似乎是正确的,所以无论如何我都会发布它。当我第一次看到你的代码示例时,我以为你正在切换BC的实例,但后来我看得更近一些。我现在有理由相信你的例子根本不会编译,但是我没有在这个盒子上有编译器来测试。

A * array = new A[1000];
array[0] = new B();
array[1] = new C();

对我来说,这看起来像是第一行分配了一个1000 A的数组。随后的两行分别对该数组的第一个和第二个元素进行操作,这些元素是A的实例,而不是指向A的指针。因此,您无法将A的指针分配给这些元素(并且new B()返回这样的指针)。类型不一样,因此它应该在编译时失败(除非A有一个带A*的赋值运算符,在这种情况下它会执行你告诉它要做的任何事情。)

那么,我完全不在基地吗?我期待找到我错过的东西。

答案 12 :(得分:-1)

如果你真的想在每个对象中保存虚拟表指针的内存,那么你可以用C风格实现代码......

E.g。

struct Point2D {
int x,y;
};

struct Point3D {
int x,y,z;
};

void Draw2D(void *pThis)
{
  Point2D *p = (Point2D *) pThis;
  //do something 
}

void Draw3D(void *pThis)
{
  Point3D *p = (Point3D *) pThis;
 //do something 
}

int main()
{

    typedef void (*pDrawFunct[2])(void *);

     pDrawFunct p;
     Point2D pt2D;
     Point3D pt3D;   

     p[0] = &Draw2D;
     p[1] = &Draw3D;    

     p[0](&pt2D); //it will call Draw2D function
     p[1](&pt3D); //it will call Draw3D function
     return 0; 
}