g++不能对具有虚拟方法的类的实例的运行时大小进行更多优化吗?

Posted

技术标签:

【中文标题】g++不能对具有虚拟方法的类的实例的运行时大小进行更多优化吗?【英文标题】:Can't the runtime size of instances of a class with virtual methods be optimized more by g++? 【发布时间】:2012-07-26 16:23:51 【问题描述】:

我刚刚用 g++ (4.7) 检查了一个包含几十个虚拟方法的类的大小,因为我听说指针用于虚拟方法,我认为这将是一个糟糕的实现,因为它会占用 80 个字节在我的系统上,一个类的每个实例只有 10 个虚拟方法。

令我欣慰的是,sizeof(<insert typename here>) 只返回了 8 个字节,即我系统上指针的大小。我认为这意味着它存储了一个指向 vtable 的指针,而不是每个方法,而且我只是误解了人们在说什么(或者也许大多数编译器都很愚蠢)。

然而,在我最终测试这个之前,我一直在努力按照我期望的方式使用虚拟方法作为指针。我注意到地址实际上是一个相对非常小的数字,通常低于 100,与其他地址相比相差 8 个字节,所以我认为它是某种数组的索引。然后我开始思考如何自己实现 vtables,并且不会使用指针,因为我的测试结果清楚地表明了这一点。我很惊讶地看到它使用了整整 8 个字节(我通过插入一个 char 字段来验证它是否只是填充,它返回 16 个字节和 sizeof)。

相反,我将通过存储一个数组索引来实现这一点(例如 4 个字节,如果使用 65536 个或更少的带有虚拟方法的类,则甚至为 2 个字节),它将在包含指向 vtable 的指针的查找表中进行搜索,并以这种方式找到它。那么为什么要存储指针呢?出于性能原因,还是他们只是将代码重用于 32 位操作系统(因为它不会影响内存大小)?

提前谢谢你。

编辑:

有人要求我计算实际节省的内存,我决定做一个代码示例。不幸的是,它变得相当大(他们要求我在两者中使用 10 个虚拟方法),但我对其进行了测试,它确实有效。来了:

#include <cstdio>
#include <cstdlib>

/* For the singleton lovers in this community */
class VirtualTableManager

    unsigned capacity, count;
    void*** vtables;
public:
    ~VirtualTableManager() 
        delete vtables;
    
    static VirtualTableManager& getInstance() 
        static VirtualTableManager instance;
        return instance;
    
    unsigned addElement(void** vtable) 
        if (count == capacity)
        
            vtables = (void***) realloc(vtables, (capacity += 0x2000) * sizeof(void**));  /* Reserves an extra 64KiB of pointers */
        
        vtables[count] = vtable;
        return count++;
    
    void** getElement(unsigned index) 
        return index < capacity ? vtables[index] : 0; /* Just in case: "Hey guys, let's misuse the API!" */
    
private:
    VirtualTableManager() : capacity(0), count(0), vtables(0)  
    VirtualTableManager(const VirtualTableManager&);
    void operator =(const VirtualTableManager&);
;

class Real

public:
    short someField; /* This is required to show the difference, because of padding */
    Real() : someField(0)  
    virtual ~Real() 
        printf("Real::~Real()\n");
    
    virtual void method0() 
        printf("Real::method0()\n");
    
    virtual void method1(short argument) 
        someField = argument;
    
    virtual short method2() 
        return someField;
    
    virtual void method3()  
    virtual void method4()  
    virtual void method5()  
    virtual void method6()  
    virtual void method7()  
    virtual void method8()  
;

class Fake

    static void** vtable;
    static unsigned classVIndex; /* Don't know what to call it, please forgive me for the lame identifier */
public:
    unsigned instanceVIndex;
    short someField;
    Fake() : instanceVIndex(classVIndex), someField(0)  
    ~Fake() 
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[9])(this);
    
    void method0() 
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[0])(this);
    
    void method1(short argument) 
        reinterpret_cast<void (*)(Fake*, short argument)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[1])(this, argument);
    
    short method2() 
        return reinterpret_cast<short (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[2])(this);
    
    void method3() 
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[3])(this);
    
    void method4() 
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[4])(this);
    
    void method5() 
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[5])(this);
    
    void method6() 
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[6])(this);
    
    void method7() 
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[7])(this);
    
    void method8() 
        reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[8])(this);
    
protected:
    Fake(unsigned instanceVIndex, short someField)
        : instanceVIndex(instanceVIndex), someField(someField)  
    /* The 'this' keyword is an automatically passed pointer, so I'll just manually pass it and identify it as 'self' (thank you, lua, I would have used something like 'vthis', which would be boring and probably incorrect) */
    static void vmethod0(Fake* self) 
        printf("Fake::vmethod0(%p)\n", self);
    
    static void vmethod1(Fake* self, short argument) 
        self->someField = argument;
    
    static short vmethod2(Fake* self) 
        return self->someField;
    
    static void vmethod3(Fake* self)  
    static void vmethod4(Fake* self)  
    static void vmethod5(Fake* self)  
    static void vmethod6(Fake* self)  
    static void vmethod7(Fake* self)  
    static void vmethod8(Fake* self)  
    static void vdestructor(Fake* self) 
        printf("Fake::vdestructor(%p)\n", self);
    
;

class DerivedFake : public Fake

    static void** vtable;
    static unsigned classVIndex;
public:
    DerivedFake() : Fake(classVIndex, 0)  
    ~DerivedFake() 
        reinterpret_cast<void (*)(DerivedFake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[1])(this);
    
    void method0() 
        reinterpret_cast<void (*)(DerivedFake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[0])(this);
    
protected:
    DerivedFake(unsigned instanceVIndex, short someField)
        : Fake(instanceVIndex, someField)  
    static void vmethod0(DerivedFake* self) 
        printf("DerivedFake::vmethod0(%p)\n", self);
    
    static void vdestructor(DerivedFake* self) 
        printf("DerivedFake::vdestructor(%p)\n", self);
        Fake::vdestructor(self); /* call parent destructor */
    
;

/* Make the vtable */
void** Fake::vtable = (void*[]) 
    (void*) &Fake::vmethod0, (void*) &Fake::vmethod1,
    (void*) &Fake::vmethod2, (void*) &Fake::vmethod3,
    (void*) &Fake::vmethod4, (void*) &Fake::vmethod5,
    (void*) &Fake::vmethod6, (void*) &Fake::vmethod7,
    (void*) &Fake::vmethod8, (void*) &Fake::vdestructor
;
/* Store the vtable and get the look-up index */
unsigned Fake::classVIndex = VirtualTableManager::getInstance().addElement(Fake::vtable);

/* Do the same for derived class */
void** DerivedFake::vtable = (void*[]) 
    (void*) &DerivedFake::vmethod0, (void*) &Fake::vmethod1,
    (void*) &Fake::vmethod2, (void*) &Fake::vmethod3,
    (void*) &Fake::vmethod4, (void*) &Fake::vmethod5,
    (void*) &Fake::vmethod6, (void*) &Fake::vmethod7,
    (void*) &Fake::vmethod8, (void*) &DerivedFake::vdestructor
;
unsigned DerivedFake::classVIndex = VirtualTableManager::getInstance().addElement(DerivedFake::vtable);

int main_virtual(int argc, char** argv)

    printf("size of 100 instances of Real including padding is %lu bytes\n"
           "size of 100 instances of Fake including padding is %lu bytes\n",
            sizeof(Real[100]), sizeof(Fake[100]));
    Real *real = new Real;
    Fake *fake = new Fake;
    Fake *derived = new DerivedFake;
    real->method1(123);
    fake->method1(456);
    derived->method1(789);
    printf("real::method2() = %hi\n"
           "fake::method2() = %hi\n"
           "derived::method2() = %hi\n", real->method2(), fake->method2(), derived->method2());
    real->method0();
    fake->method0();
    derived->method0();
    delete real;
    delete fake;
    delete derived;
    return 0;

不用担心,我通常不会将定义放在这样的类中。我只是在这里这样做是为了希望提高可读性。无论如何,输出:

size of 100 instances of Real including padding is 1600 bytes
size of 100 instances of Fake including padding is 800 bytes
real::method2() = 123
fake::method2() = 456
derived::method2() = 789
Real::method0()
Fake::vmethod0(0x1bd8040)
DerivedFake::vmethod0(0x1bd8060)
Real::~Real()
Fake::vdestructor(0x1bd8040)
DerivedFake::vdestructor(0x1bd8060)
Fake::vdestructor(0x1bd8060)

它可能不是线程安全的,可能包含大量可怕的错误,也可能效率相对较低,但我希望它能够展示我的概念。它在 64 位 Ubuntu 上使用 g++-4.7 进行了测试。我怀疑 32 位系统在大小上是否有任何好处,因为我节省了不到一个字(4 个字节,这么多!)我不得不在其中放置一个字段以显示效果。随意对速度进行基准测试(如果你这样做,请先优化它,我匆忙这样做)或在其他架构/平台和其他编译器上测试效果(我想看看结果,所以如果你这样做,请分享它们)。当有人发现需要创建一个 128/256 位平台,创建一个内存支持非常有限但速度令人难以置信的处理器,或者使用每个实例上的 vtable 使用 21 个字节的编译器时,类似的东西可能会很有用。

编辑:

哎呀,代码示例是一个 derp。修好了。

【问题讨论】:

这可能会在代码中产生难以处理的奇怪副作用。例如,由于添加了新的虚拟类,类中数据成员的偏移量会更改其他内容。此外,空间换取了时间,因为一个虚方法调用现在需要两个 derference。 @user315052- 不是已经需要两个取消引用吗?一个用于 vtable 指针,一个用于函数起始地址? 这会造成额外的取消引用。 @RPFeltz 我看到您已经接受了一个答案,但是您能否在您的问题中添加计算可以节省多少内存的问题。例如,您有一个包含 100 个类的应用程序,每个类有 10 个虚拟方法。如果使用 vtables,您的应用程序将使用 100 * 10 * sizeof(function pointer) 字节来存储 vtables。如果使用您的想法,您可以节省多少内存? @skwllsp 我指的是将类匹配到正确的 vtable 的方法,而不是存储实际的函数指针(我曾经认为它存储在实际实例中)。它目前似乎是一个指向包含函数指针的表的指针,我提出了一种方法来进一步减小大小,方法是使用包含指向包含函数指针的表的指针的表的索引。您是否想要一个代码示例来说明在创建实例时如何节省内存? 【参考方案1】:

使用基于数组的 vtable 的一个挑战是如何将多个已编译的源文件链接在一起。如果每个编译文件都存储自己的表,则链接器在生成最终二进制文件时必须将这些表组合在一起。这增加了链接器的复杂性,现在必须让链接器了解这个新的 C++ 特定细节。

此外,您描述的字节节省技术很难正确处理多个编译单元。如果你有两个源文件,每个源文件的类都足够少,每个 vtable 索引使用两个字节,但现在合并起来需要三个字节怎么办?在这种情况下,链接器必须根据新的对象大小重写对象文件。

此外,这个新系统无法与动态链接很好地交互。如果您有一个在运行时链接的单独的目标文件,您将有两个或更多的 vtables 全局表。然后生成的目标代码必须考虑到这一点,这会增加代码生成器的复杂性。

最后,还有对齐问题。当字长为 8 个字节时,使用两个或四个字节作为索引可能会降低程序性能,如果它偏移了对象的所有其他字段。事实上,g++ 完全有可能只使用四个字节,然后填充到八个。

简而言之,您没有理由不进行此优化,但它的实现非常复杂,并且(可能)成本很高。也就是说,这是一个非常聪明的主意!

希望这会有所帮助!

【讨论】:

感谢您提供这个伟大而最令人满意的答案!我已经在怀疑可变索引大小了,但是 4 个字节足以让它工作,而且仍然是一个小小的奖励。但是,动态链接问题肯定是我没有想到的一个有效点,并且我相信它使整个方法毫无意义,因为它需要额外的信息(除非您将表连接到某处并通过使数组位置牺牲更多的性能速度一个指针)。我还考虑了使用字长的性能奖励的可能性,这验证了它。【参考方案2】:

这总是一个权衡。要成为一种改进,任何节省空间的方案都必须至少经常节省空间并且永远失去速度。

如果您在类中放置一个 2 或 4 字节的索引,然后我添加一个指针作为第一个成员,则必须有一些填充才能使我的指针正确对齐。

所以现在这个类是 16 字节。如果索引比使用 vtable 指针甚至 稍微 慢,那就是净损失。

我可以接受这并不总是缩小尺寸,但我不想因为没有尺寸增加而损失一些速度。

【讨论】:

【参考方案3】:

此外,CPU 预取简单地址而不是数组索引更简单(当然,还有额外的取消引用)。您增加的成本将超过一次取消引用的成本。

【讨论】:

以上是关于g++不能对具有虚拟方法的类的实例的运行时大小进行更多优化吗?的主要内容,如果未能解决你的问题,请参考以下文章

类加载

python中的类

Activator.CreateInstance - 如何创建具有参数化构造函数的类的实例

抽象类

关键字Sealed

Python --类和实例