17

最近、C++の仮想化によるメモリオーバーヘッドについての質問を投稿しました。答えにより、vtableとvptrがどのように機能するかを理解できます。私の問題は次のとおりです。私はスーパーコンピューターで作業しています。何十億ものオブジェクトがあり、その結果、仮想性によるメモリのオーバーヘッドを気にする必要があります。いくつかの対策の後、仮想関数でクラスを使用すると、各派生オブジェクトには8バイトのvptrがあります。これはまったく無視できません。

Intelicpcまたはg++に、vptrの代わりに調整可能な精度で「グローバル」vtablesおよびインデックスを使用するための構成/オプション/パラメーターがあるかどうか疑問に思います。そのようなことで、数十億のオブジェクトに対して8バイトのvptrの代わりに2バイトのインデックス(unsigned short int)を使用できるようになるためです(そしてメモリオーバーヘッドが大幅に削減されます)。コンパイルオプションを使用してそれ(またはそのようなもの)を行う方法はありますか?

どうもありがとうございます。

4

1 に答える 1

17

残念ながら...自動的ではありません。

ただし、vテーブルは、実行時のポリモーフィズムの構文糖衣にすぎないことを忘れないでください。コードを再設計する場合は、いくつかの方法があります。

  1. 外部ポリモーフィズム
  2. 手作りのVテーブル
  3. 手作りのポリモーフィズム

1)外部ポリモーフィズム

アイデアは、一時的な方法でのみポリモーフィズムが必要になる場合があるということです。つまり、たとえば、次のようになります。

std::vector<Cat> cats;
std::vector<Dog> dogs;
std::vector<Ostrich> ostriches;

void dosomething(Animal const& a);

動的タイプ(値によって格納される)を知っているので、この状況に仮想ポインターを埋め込むことは無駄であるCatか、またはそのように思われます。Dog

外部ポリモーフィズムとは、純粋なコンクリートタイプと純粋なインターフェイス、およびコンクリートタイプをインターフェイスに一時的に(または永続的に、ただしここでは必要ない)適応させるための単純なブリッジを中央に配置することです。

// Interface
class Animal {
public:
    virtual ~Animal() {}

    virtual size_t age() const = 0;
    virtual size_t weight() const = 0;

    virtual void eat(Food const&) = 0;
    virtual void sleep() = 0;

private:
    Animal(Animal const&) = delete;
    Animal& operator=(Animal const&) = delete;
};

// Concrete class
class Cat {
public:
    size_t age() const;
    size_t weight() const;

    void eat(Food const&);
    void sleep(Duration);
};

橋は一度だけ書かれています:

template <typename T>
class AnimalT: public Animal {
public:
    AnimalT(T& r): _ref(r) {}

    virtual size_t age() const override { return _ref.age(); }
    virtual size_t weight() const { return _ref.weight(); }

    virtual void eat(Food const& f) override { _ref.eat(f); }
    virtual void sleep(Duration const d) override { _ref.sleep(d); }

private:
    T& _ref;
};

template <typename T>
AnimalT<T> iface_animal(T& r) { return AnimalT<T>(r); }

そして、あなたはそれをそう使うことができます:

for (auto const& c: cats) { dosomething(iface_animal(c)); }

アイテムごとに2つのポインターのオーバーヘッドが発生しますが、ポリモーフィズムが必要な場合に限ります。

別の方法は、AnimalT<T>(参照ではなく)値も処理cloneし、状況に応じてvポインターを使用するかどうかを完全に選択できるメソッドを提供することです。

この場合、簡単なクラスを使用することをお勧めします。

template <typename T> struct ref { ref(T& t): _ref(t); T& _ref; };

template <typename T>
T& deref(T& r) { return r; }

template <typename T>
T& deref(ref<T> const& r) { return r._ref; }

次に、ブリッジを少し変更します。

template <typename T>
class AnimalT: public Animal {
public:
    AnimalT(T r): _r(r) {}

    std::unique_ptr< Animal<T> > clone() const { return { new Animal<T>(_r); } }

    virtual size_t age() const override { return deref(_r).age(); }
    virtual size_t weight() const { return deref(_r).weight(); }

    virtual void eat(Food const& f) override { deref(_r).eat(f); }
    virtual void sleep(Duration const d) override { deref(_r).sleep(d); }

private:
    T _r;
};

template <typename T>
AnimalT<T> iface_animal(T r) { return AnimalT<T>(r); }

template <typename T>
AnimalT<ref<T>> iface_animal_ref(T& r) { return Animal<ref<T>>(r); }

このようにして、多形ストレージが必要な場合と必要でない場合を選択します。


2)手作りのVテーブル

(閉じた階層でのみ簡単に機能します)

Cでは、独自のvテーブルメカニズムを提供することでオブジェクト指向をエミュレートするのが一般的です。Vテーブルとは何か、Vポインターがどのように機能するかを知っているように見えるので、自分で完全に機能させることができます。

struct FooVTable {
    typedef void (Foo::*DoFunc)(int, int);

    DoFunc _do;
};

Foo次に、 :に固定された階層のグローバル配列を提供します。

extern FooVTable const* const FooVTableFoo;
extern FooVTable const* const FooVTableBar;

FooVTable const* const FooVTables[] = { FooVTableFoo, FooVTableBar };

enum class FooVTableIndex: unsigned short {
    Foo,
    Bar
};

次に、クラスで必要なのはFoo、最も派生したタイプを保持することだけです。

class Foo {
public:

    void dofunc(int i, int j) {
        (this->*(table()->_do))(i, j);
    }

protected:
    FooVTable const* table() const { return FooVTables[_vindex]; }

private:
    FooVTableIndex _vindex;
};

閉じた階層は、階層のすべてのタイプを認識する必要があるFooVTables配列と列挙のためにあります。FooVTableIndex

ただし、列挙型インデックスはバイパスできます。配列を非定数にすることで、事前に初期化してより大きなサイズにし、初期化に各派生型を自動的に登録することができます。したがって、この初期化フェーズ中にインデックスの競合が検出され、自動解決(アレイをスキャンして空きスロットを探す)を行うことも可能です。

これはあまり便利ではないかもしれませんが、階層を開く方法を提供します。ここではグローバル変数について話しているので、スレッドを起動する前にコーディングする方が明らかに簡単です。


3)手作りのポリモーフィズム

(実際には閉じた階層でのみ機能します)

後者は、LLVM/Clangコードベースを調査した私の経験に基づいています。コンパイラには、直面しているのとまったく同じ問題があります。数万または数十万の小さなアイテムの場合、アイテムごとのvpointerは実際にメモリ消費を増やし、これは煩わしいことです。

したがって、彼らは単純なアプローチを取りました。

  • 各クラス階層には、enumすべてのメンバーをリストするコンパニオンがあります
  • 階層内の各クラスは、enumerator構築時にそのコンパニオンをベースに渡します
  • 仮想化は、を切り替えてenum適切にキャストすることで実現されます

コード内:

enum class FooType { Foo, Bar, Bor };

class Foo {
public:
    int dodispatcher() {
        switch(_type) {
        case FooType::Foo:
            return static_cast<Foo&>(*this).dosomething();

        case FooType::Bar:
            return static_cast<Bar&>(*this).dosomething();

        case FooType::Bor:
            return static_cast<Bor&>(*this).dosomething();
        }
        assert(0 && "Should never get there");
    }
private:
    FooType _type;
};

スイッチはかなり面倒ですが、いくつかのマクロやタイプリストを使って多かれ少なかれ自動化されている可能性があります。LLVMは通常、次のようなファイルを使用します。

 // FooList.inc
 ACT_ON(Foo)
 ACT_ON(Bar)
 ACT_ON(Bor)

そして、あなたはします:

 void Foo::dodispatcher() {
     switch(_type) {
 #   define ACT_ON(X) case FooType::X: return static_cast<X&>(*this).dosomething();

 #   include "FooList.inc"

 #   undef ACT_ON
     }

     assert(0 && "Should never get there");
 }

Chris Lattnerは、スイッチの生成方法(コードオフセットのテーブルを使用)により、仮想ディスパッチと同様のコードが生成され、CPUオーバーヘッドはほぼ同じですが、メモリオーバーヘッドが低くなるとコメントしました。

明らかに、1つの欠点は、派生クラスのすべてFoo.cppのヘッダーを含める必要があることです。これは、階層を効果的に封印します。


私は、最もオープンなものから最もクローズドなものまで、自主的に解決策を提示しました。それらにはさまざまな程度の複雑さ/柔軟性があり、どれがあなたに最も適しているかを選択するのはあなた次第です。

重要なことの1つは、後者の2つのケースでは、破棄とコピーに特別な注意が必要です。

于 2012-05-12T10:15:34.233 に答える