【问题标题】:C++ virtual function table memory costC++虚函数表内存开销
【发布时间】:2010-12-10 05:21:27
【问题描述】:

考虑:

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 个 double 成员所需的大小,加上虚函数指针表所需的一些开销。现在,回到我的代码,原来 'sizeof(myclass)' 是 32;也就是说,我的数据成员使用 24 个字节,虚函数表(4 个虚函数)使用 8 个字节。我的问题是:有什么办法可以简化这个吗?我的程序最终会使用大量内存,我不喜欢 25% 的内存被虚函数指针吃掉的声音。

【问题讨论】:

  • 不要忘记虚拟析构函数。
  • 查看您的分配策略,您最好调查使用来自每个B、C 等的自定义分配器。对大量单个对象使用基本new 可能增加的开销比您意识到的要多。
  • 如答案所述,您只需为每个实例支付一个指针(x86 上为 4 个字节,x64 上为 8 个字节)。但是,内存对齐可以稍微改变一下情况(例如,如果您有一个具有一个 char 和虚拟方法的类,它通常在 x86 上占用 8 个字节)。此外,Microsoft C++ 编译器在对齐 vfptr 时存在已知错误,该错误可以为类大小增加 4 个左右的字节。
  • 您还应该考虑到,因为一个通用的 new 可能会处理多种类型的对象,它可能会分配更多 sizeof 需要的...

标签: c++ memory virtual


【解决方案1】:

v-table 每个类而不是每个对象。每个对象只包含一个指向其 v-table 的 指针。所以每个实例的开销是sizeof(pointer)(通常是 4 或 8 个字节)。对于类对象的大小,您有多少虚拟函数并不重要。考虑到这一点,我认为您不必太担心。

【讨论】:

  • 只是添加它 - 每个类仍然有一个 v-table,每个虚拟函数至少包含一个指针。
  • 您仍然需要为 - 在许多情况下完全没有必要的 - vtable 支付额外费用。它只是在可执行文件的“全局”位中填充每个类的空间,而不是每个实例。所述空间可能非常重要,特别是如果在不需要时从具有大量虚拟函数的类继承。在这种情况下,切换到组合可以显着减少二进制文件。通过停止不必要地从某人的库类型继承,我将一个项目的二进制文件缩减了 1/6。此外,它还有一个通常的好处,就是将您的类设计从所有其他有据可查的继承约束中解放出来
【解决方案2】:

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

没有办法解决这个问题,但请记住(通常情况下)每个虚函数表在类的所有实例之间共享,因此一旦您支付了多个虚函数或额外的继承级别,就不会产生很大的开销“vptr tax”(vtable 指针的小成本)。

对于较大的类,开销的百分比会小得多。

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 通过将 vtable 指针放在类的头部没有对齐问题(这就是大多数编译器将它放在那里的原因)。
    • 如果您的类大小小于一开始的 vtable 指针,对齐问题仍然存在。假设struct C { char c; } 的大小可能为 1,任何这样的数组都会将它们紧紧地包装在一起。如果添加一个虚拟成员,在 32 位平台上,它可能会将生成的 5 个字符的存储空间填充到 8 个,所以现在每个对象突然浪费了 3 个字符。
    • 这个答案明确提到了“对齐”,这确实是内存很少的目标(微控制器)的问题。就像 Pavel 描述的那样,一个 char + vtable 指针(4 个字节)在 32 位 Cortex-Mx ARM 上产生 8 个字节(在 vtable ptr 之前可能/希望有 3 个字节的填充)。
    • @MartinYork 大多数,但并非所有编译器都将 vtable 放在开头?
    【解决方案4】:

    你有两个选择。

    1) 别担心。

    2) 不要使用虚函数。但是,不使用虚函数只会将大小转移到您的代码中,因为您的代码会变得更加复杂。

    【讨论】:

      【解决方案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 太有用了)。

      【讨论】:

      • 严格来说,原版连编译都不会。当数组元素不是指针时,它会尝试将 pointer 值分配给数组元素。在代码变得可编译之前,现在说它有“切片问题”还为时过早。
      • @AndretT:是的。跳了枪。
      【解决方案6】:

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

      请注意,即使有 100 万个实例,我们所说的总大小也是 32MB。高达 1000 万,别担心。

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

      请注意,考虑到双精度数的对齐方式,即使是单个字节作为类型标识符也会使数组元素大小增加到 32。因此,Stjepan Rajko 的解决方案在某些情况下很有帮助,但对您的情况却没有。

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

      【讨论】:

        【解决方案7】:

        如果您将拥有数百万个这样的东西,并且内存对您来说是一个严重的问题,那么您可能不应该将它们设为对象。只需将它们声明为结构或 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 */
        

        【讨论】:

          【解决方案8】:

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

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

          【讨论】:

            【解决方案9】:

            如果你事先知道所有派生类型及其各自的更新函数,你可以将派生类型存储在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;
                // ...
                }
            }
            

            【讨论】:

              【解决方案10】:

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

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

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

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

              【讨论】:

                【解决方案11】:

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

                【讨论】:

                • 我会说他们可能不会这样做。 C++ 允许在同一对象的数据成员地址之间进行指针比较(有一些限制),要求此类比较的结果反映声明的顺序。我会说,任何一种通过重新排列的数据成员来支持这一要求所必需的“编译器魔法”都太昂贵了。
                • 我认为任何两个指针在算术上都是可比的,但 C++ 的输入有时需要你进行转换。无论哪种方式,都会计算进入这样一个系统的“编译器魔法”,以最大限度地减少整个类对象的内部碎片,而不是在每个实例的基础上完成。这都是假设我没有误解你的评论。
                【解决方案12】:

                鉴于所有已经在这里的答案,我想我一定是疯了,但这对我来说似乎是正确的,所以我还是发布了它。当我第一次看到您的代码示例时,我以为您正在切片 B 和 C 的实例,但后来我仔细看了看。我现在有理由确定您的示例根本无法编译,但我没有在这个盒子上安装编译器来测试。

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

                在我看来,第一行分配了一个包含 1000 个 A 的数组。随后的两行分别对该数组的第一个和第二个元素进行操作,它们是A 的实例,而不是指向A 的指针。因此,您不能将指向 A 的指针分配给这些元素(并且 new B() 返回这样的指针)。类型不一样,因此它应该在编译时失败(除非A 有一个赋值运算符接受A*,在这种情况下,它会按照您的要求执行)。

                那么,我完全不在基地吗?我期待着找出我错过了什么。

                【讨论】:

                  【解决方案13】:

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

                  例如

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

                  【讨论】:

                  • 这不是答案。关于 funcptrs 的教程如何相关?在现实生活中需要虚拟功能的情况下如何替代它,例如OP 创建/迭代异构相关对象容器的示例?每个 obj 如何知道应该为它调用哪个函数?它需要某种...功能表。任何试图实现多态性或更广泛的 OOP(如 C 语言)的人最终都会@ 与他们刚刚使用像 vfunc 之类的原生 C++ 东西一样的开销水平,但现在没有人能够理解他们的代码。 (c.f. C OOP 库:令人印象深刻,但读起来很麻烦)
                  猜你喜欢
                  • 1970-01-01
                  • 2011-03-09
                  • 1970-01-01
                  • 2013-03-22
                  • 1970-01-01
                  • 2010-12-31
                  • 2015-02-12
                  • 1970-01-01
                  • 2011-10-09
                  相关资源
                  最近更新 更多