4

私はしばらくの間、プログラミング言語のアイデアをいじっていました。それは基本的に、システムプログラミング(または実際には高性能を必要とするすべてのプログラミング)を対象とした構文でC ++およびJavaに似ていますが、私の意見では、 、C++ より楽しい構文です。階層クラス構造で仮想メソッドを処理する方法 (私の言語には多重継承は含まれない) と、vtable ルックアップを回避する方法について考えていました。私の質問は 2 つあります。

  1. 私の理解では、vtable ルックアップが (少なくともゲーム開発のようなタイム クリティカルなシナリオでは) パフォーマンスに影響を与える理由は、オブジェクトの vtable ポインターを参照する必要があり、この vtable は一般的にキャッシュ ミスであるためです。これは正しいですか、それとも問題の一部が欠けていますか?
  2. 部分的な解決策についての私の考えは次のとおりです。コンパイラがオブジェクトの型を完全に決定できる場合(つまり、それが考えている型から派生した型であることができない場合)、このオブジェクトは、型が次の引数として関数に渡されますオブジェクトの型のスーパークラスの場合、関数で呼び出される仮想メソッドの場所は、コンパイル時に追加される一種の「隠し」引数として渡すことができます。おそらく例が役立つでしょう:

クラス階層の次の疑似コードを検討してください。

class Animal {
    public void talk() { /* Generic animal noise... */ }
    // ...
}

class Dog extends Animal {
    public void talk() { /* Override of Animal::talk(). */ }
    // ...
}

void main() {
    Dog d = new Dog();
    doSomethingWithAnimal(d);
}

void doSomethingWithAnimal(Animal a) {
    // ...
    a.talk();
    // ....
}

これは疑似コードであり、C++ や Java などではありません。また、Animal 引数は、値ではなく参照によって暗黙的に渡されると仮定します。コンパイラはそれdが間違いなく typeであることを確認できるため、定義を次のようにDog変換できます。doSomethingWithAnimal

void doSomethingWithAnimal(Animal a, methodptr talk = NULL) {
    // ...
    if ( talk != NULL ) {
        talk(a);
    } else {
        a.talk();
    }
    // ...
}

次にmain、コンパイラによって次のように変換されます。

void main() {
    Dog d = new Dog();
    doSomethingWithAnimal(d, Dog::talk);
}

明らかに、これで vtable の必要性が完全になくなるわけではありません。また、オブジェクトの正確なタイプを特定できない場合に備えて vtable を提供する必要があるかもしれませんが、パフォーマンスの最適化としてこれについてどう思いますか? 可能な限りレジスタを使用して引数を渡す予定です。引数がスタックにあふれたとしても、スタック上の methodptr 引数は vtable 値よりもキャッシュ ヒットになる可能性が高くなりますよね? どんな考えも大歓迎です。

4

1 に答える 1

9

Q1 に関して:キャッシュの使用率は、実際には仮想呼び出しの「問題」の一部にすぎません。関数の全体的なポイントvirtual、および一般的な遅延バインディングは、呼び出しサイトが変更なしで任意の実装を呼び出すことができるということです。これには、いくつかの間接化が必要です。

  • インダイレクションは、インダイレクションを解決するためのスペースや時間のオーバーヘッドを意味します。
  • どのように間接呼び出しを行っても、間接呼び出しは、CPU に適切な分岐予測子があり、呼び出しサイトが単一型 (つまり、1 つの実装のみが呼び出される) である場合にのみ、静的呼び出しと同じくらい高速になります。また、完全に予測されたブランチが、すべてのハードウェア開発者が関心を持っている静的ブランチと同じくらい高速かどうかもわかりません。
  • コンパイル時に呼び出された関数がわからないと、呼び出された関数を知っていることに基づく最適化も抑制されます (インライン化だけでなく、ループ不変コードの動きなども)。

あなたのアプローチはそれを変更しないため、パフォーマンスの問題のほとんどをそのまま残します:それでもいくらかの時間とスペースを浪費し (vtable ルックアップではなく、追加の引数とブランチでのみ)、インライン化やその他の最適化を許可しません。間接呼び出しは削除されません。

Re 2:これは、C++ コンパイラが既にある程度 (コメントで @us2012 によって説明されているように制限付きで) 既に行われている、非仮想化の手続き間のスピンのようなものです。それにはいくつかの「マイナーな」問題がありますが、選択的に適用すれば価値があるかもしれません. そうしないと、さらに多くのコードを生成し、多くの追加の引数を渡し、多く追加の分岐を実行し、わずかしか得られないか、純損失さえ発生します。

主な問題は、上記のパフォーマンスの問題のほとんどが解決されないことだと思います。サブクラスや同じテーマの他のバリエーションに特化した関数 (1 つの汎用本体ではなく) を生成すると、これに役立つ場合があります。しかし、それはパフォーマンスの向上でそれ自体を正当化しなければならない追加のコードを生成し、一般的なコンセンサスは、パフォーマンスが重要なプログラムであっても、ほとんどのコードにとってそのような積極的な最適化は価値がないということです.

特に、仮想呼び出しのオーバーヘッドは、まったく同じ機能のベンチマークでのみ問題になります。または、他のすべてのことから常に愛する地獄を既に最適化しており、大量の小さな間接呼び出しが必要な場合 (ゲーム開発の例: 複数の仮想メソッド)描画または錐台カリングのためのジオメトリ オブジェクトごとの呼び出し)。ほとんどのコードでは、仮想呼び出しは問題にならないか、少なくともさらに最適化を試みるほどではありません。さらに、JIT コンパイラーにはこれらの問題を処理する他の方法があるため、これは AOT コンパイラーにのみ関連します。ポリモーフィックなインライン キャッシュを調べてください。トレース JIT コンパイラは、仮想かどうかに関係なく、すべての呼び出しを単純にインライン化できることに注意してください。

要約すると、vtables はすでに仮想関数を実装するための高速で一般的な方法です (使用できる場合、ここではそうです)。おそらくいくつかのまれなケースを除いて、改善に気付くことは言うまでもなく、それらを大幅に改善できる可能性は低いです. ただし、試してみたい場合は、このようなことを行う LLVM パスを作成してみてください (ただし、より低いレベルの抽象化に取り組む必要があります)。

于 2013-01-15T20:03:41.660 に答える