3

ポインタが仮想メソッドに使用されると聞いていたので、g ++(4.7)を使用して数十の仮想メソッドを含むクラスのサイズを確認しました。これは、インスタンスごとに80バイトを使用するため、ひどい実装になると思いました。私のシステムでは、仮想メソッドが10個しかないクラスです。

安心sizeof(<insert typename here>)して、システム上のポインタのサイズである8バイトしか返しませんでした。これは、各メソッドではなく、vtableへのポインターが格納されていること、および人々が言っ​​ていることを単に誤解していることを意味していると思います(または、ほとんどのコンパイラーは愚かです)。

しかし、最終的にこれをテストする前は、仮想メソッドをポインターとして使用して、期待どおりに機能することに苦労していました。アドレスが実際には比較的非常に少ない数であり、多くの場合100未満であり、他のアドレスと比較して8バイトの違いがあることに気付いたので、それは配列のある種のインデックスであると思いました。次に、テストの結果が明確に示しているように、vtableを自分で実装する方法について考えましたが、ポインターは使用しませんでした。全体で8バイトを使用していることに驚きました(sizeofで16バイトを返すcharフィールドを挿入することで、パディングだけであるかどうかを確認しました)。

代わりに、vtablesへのポインターを含むルックアップテーブルで検索される配列インデックス(たとえば、4バイト、または仮想メソッドを持つ65536以下のクラスが使用されている場合は2バイト)を格納することでこれを実装し、そのように。では、なぜポインタが格納されるのでしょうか。パフォーマンス上の理由から、または単に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)

スレッドセーフではない可能性があり、恐ろしいバグの群れが含まれている可能性があり、比較的非効率的である可能性もありますが、それが私の概念を示していることを願っています。g++-4.7を搭載した64ビットUbuntuでテストされました。32ビットシステムにサイズの利点があるとは思えません。1ワード未満(4バイト、それだけです!)節約できるので、効果を表示するためにそこにフィールドを配置する必要がありました。ただし、速度のベンチマークを自由に行ってください(最適化する場合は、最初に最適化してください。急いでください)。または、他のアーキテクチャ/プラットフォームや他のコンパイラでの効果をテストしてください(結果を確認したいので、共有する場合は共有してください)。 )。128/256ビットプラットフォームを作成する必要がある場合、メモリサポートが非常に限られているが信じられないほどの速度のプロセッサを作成する場合、または各インスタンスのvtableに21バイトを使用するコンパイラを使用する場合は、同様のことが役立つ場合があります。

編集:

おっと、コード例はderpでした。修正しました。

4

3 に答える 3

5

配列ベースのvtableの課題の1つは、コンパイルされた複数のソースファイルをどのようにリンクするかです。コンパイルされた各ファイルに独自のテーブルが格納されている場合、リンカは最終的なバイナリを生成するときにそれらのテーブルを組み合わせる必要があります。これにより、リンカーの複雑さが増します。リンカーは、この新しいC++固有の詳細を認識している必要があります。

さらに、あなたが説明したバイト節約のテクニックは、複数のコンパイルユニットを正しく理解するのが難しいでしょう。2つのソースファイルがあり、それぞれにvtableインデックスごとに2バイトを使用するのに十分なクラスがほとんどないが、それらを組み合わせると3バイトが必要になった場合はどうなりますか?その場合、リンカは新しいオブジェクトサイズに基づいてオブジェクトファイルを書き換える必要があります。

さらに、この新しいシステムはダイナミックリンクとうまく相互作用しません。実行時にリンクされた別のオブジェクトファイルがある場合、vtablesの2つ以上のグローバルテーブルがあります。生成されたオブジェクトコードはこれを考慮に入れる必要があり、コードジェネレータの複雑さが増します。

最後に、配置の問題があります。ワードサイズが8バイトのときにインデックスに2バイトまたは4バイトを使用すると、オブジェクトの他のすべてのフィールドをオフセットすると、プログラムのパフォーマンスが低下する可能性があります。実際、g ++が4バイトしか使用しない可能性は完全にありますが、その後8バイトにパディングされます。

要するに、この最適化を実行できなかった理由はありませんが、実装が大幅に複雑になり、(おそらく)実行コストがかかります。そうは言っても、それは非常に賢いアイデアです!

お役に立てれば!

于 2012-07-26T16:41:18.437 に答える
1

これは常にトレードオフです。改善するためには、スペースを節約するためのスキームは、少なくとも頻繁にスペースを節約する必要があり、速度を失うことはありません。

クラスに2バイトまたは4バイトのインデックスを配置し、最初のメンバーとしてポインターを追加した場合、ポインターを正しく配置するには、パディングが必要になります。

したがって、クラスはとにかく16バイトになります。インデックス作成がvtableポインターを使用するよりもわずかに遅い場合は、正味の損失になります。

必ずしもサイズの縮小ではないことは承知していますが、サイズが大きくならないために速度を落としたくありません。

于 2012-07-26T18:33:13.293 に答える
0

さらに、CPUは、配列へのインデックスではなく、単純なアドレスをプリフェッチする方が簡単です(もちろん、追加の参照解除も可能です)。1回の参照解除のコスト以上のものを追加します。

于 2012-07-26T16:44:45.537 に答える