使用虚拟方法的类的实例的运行时大小是否不能通过g ++进行更优化?

时间:2012-07-26 16:23:51

标签: c++ optimization gcc size vtable

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

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

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

相反,我会通过存储一个数组索引(例如4个字节,甚至2个,如果使用带有虚方法的65536或更少的类)来实现它,这将在包含指向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 。修正了它。

3 个答案:

答案 0 :(得分:5)

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

此外,您所描述的节省字节的技术对于使用多个编译单元来说是很棘手的。如果你有两个源文件,每个源文件足够少,每个vtable索引使用两个字节,但现在需要三个字节组合怎么办?在这种情况下,链接器必须根据新的对象大小重写目标文件。

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

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

简而言之,没有理由不能进行这种优化,但是它会带来很大的实现复杂性,并且(可能)会产生严重的成本。那就是说,这是一个非常聪明的主意!

希望这有帮助!

答案 1 :(得分:1)

这总是一种权衡。要做出改进,任何节省空间的方案都必须至少经常节省空间而永远不会失去速度。

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

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

我可以接受它并不总是减小尺寸,但我不想失去一些没有增益的速度。

答案 2 :(得分:0)

此外,CPU更容易预取一个简单的地址而不是数组的索引(当然还有额外的de-references)。您将添加超过单个去参考的成本。