5

私は、自殺チェスと敗者チェスを通常のチェスと一緒にプレイするチェスバリアントエンジンを持っています。時間が経つにつれて、エンジンにさらにバリエーションを追加する可能性があります。エンジンは、OOP を適切に使用して C++ で完全に実装されています。私の質問は、そのようなバリアント エンジンの設計に関するものです。

当初、このプロジェクトは自殺専用のエンジンとして開始されましたが、時間の経過とともに他のフレーバーを追加しました。新しいバリアントを追加するために、まず C++ でポリモーフィズムを使用して実験しました。たとえば、MoveGenerator抽象クラスには 2 つのサブクラスがSuicideMoveGeneratorありNormalMoveGenerator、ユーザーが選択したゲームの種類に応じて、ファクトリが適切なサブクラスをインスタンス化します。しかし、これははるかに遅いことがわかりました。明らかに、仮想関数を含むクラスをインスタンス化することと、タイトなループで仮想関数を呼び出すことはどちらも非常に非効率的だからです。

しかし、コードを最大限に再利用してさまざまなバリアントのロジックを分離するために、テンプレートの特殊化を伴う C++ テンプレートを使用することに気がつきました。ゲームのタイプを選択すると、基本的にゲームの最後までそれを使い続けるため、コンテキストでは動的リンクは実際には必要ないため、これも非常に論理的に思えました。C++ テンプレートの特殊化はまさにこれを提供します - 静的ポリモーフィズム。テンプレート パラメータは、 または または のいずれSUICIDELOSERSですNORMAL

enum GameType { LOSERS, NORMAL, SUICIDE };

したがって、ユーザーがゲームの種類を選択すると、適切なゲーム オブジェクトがインスタンス化され、そこから呼び出されるすべてのものが適切にテンプレート化されます。たとえば、ユーザーが自殺チェスを選択した場合、次のように言いましょう。

ComputerPlayer<SUICIDE>

オブジェクトはインスタンス化され、そのインスタンス化は基本的に制御フロー全体に静的にリンクされます。の関数はなどでComputerPlayer<SUICIDE>動作しますがMoveGenerator<SUICIDE>Board<SUICIDE>対応する関数NORMALは適切に動作します。

全体として、これにより、最初に適切なテンプレート化特殊クラスをインスタンス化でき、他のif条件がどこにもないため、全体が完全に機能します。最高のことは、パフォーマンスの低下がまったくないことです。

ただし、このアプローチの主な欠点は、テンプレートを使用するとコードが読みにくくなることです。また、テンプレートの特殊化を適切に処理しないと、重大なバグが発生する可能性があります。

他のバリアント エンジンの作成者は通常、ロジックを分離するために (コードを適切に再利用して) 何をしているのだろうか?? C++ テンプレート プログラミングが非常に適していることがわかりましたが、他にもっと良いものがあれば、喜んで受け入れます。特に、HG Muller 博士による Fairymax エンジンをチェックしましたが、ゲームのルールを定義するために構成ファイルを使用します。私のバリアントの多くは異なる拡張機能を持っており、構成ファイルのレベルまで一般化することでエンジンが強力に成長しない可能性があるため、私はそれをしたくありません。別の人気のあるエンジン Sjeng は、ifいたるところに条件が散らばっており、個人的には良い設計ではないと感じています。

新しいデザインの洞察は非常に役に立ちます。

4

2 に答える 2

6

「タイトなループで仮想関数を呼び出すのは非効率的です」

これが実際の肥大化を引き起こした場合、ループのすべての変数が同じ動的タイプを持っている場合、コンパイラが対応する命令をL1キャッシュからフェッチすることを期待しているので、それほど苦しむことはありません。

しかし、私が心配している部分が1つあります。

「明らかに、仮想関数を含むクラスのインスタンス化は非常に非効率的だからです」

今...本当にびっくりしました。

仮想関数を使用してクラスをインスタンス化するコストは、仮想関数を使用せずにクラスをインスタンス化するコストとほとんど区別できません。これはもう1つのポインターであり、それだけです(一般的なコンパイラーでは、に対応します_vptr)。

あなたの問題は他の場所にあると思います。だから私は大げさな推測をするつもりです:

  • 万が一、動的なインスタンス化がたくさん行われていますか?(呼び出しnew

その場合は、それらを削除することで多くの利益を得ることができます。

Strategyあなたの正確な状況に非常に適していると呼ばれるデザインパターンがあります。このパターンの考え方は、実際には仮想関数の使用に似ていますが、実際にはそれらの関数を外部化します。

簡単な例を次に示します。

class StrategyInterface
{
public:
  Move GenerateMove(Player const& player) const;
private:
  virtual Move GenerateMoveImpl(Player const& player) const = 0;
};

class SuicideChessStrategy: public StrategyInterface
{
  virtual Move GenerateMoveImpl(Player const& player) const = 0;
};

// Others

実装したら、適切な戦略を立てるための関数が必要です。

StrategyInterface& GetStrategy(GameType gt)
{
  static std::array<StrategyInterface*,3> strategies
    = { new SuicideChessStrategy(), .... };
  return *(strategies[gt]);
}

そして最後に、他の構造に継承を使用せずに作業を委任できます。

class Player
{
public:
  Move GenerateMove() const { return GetStrategy(gt).GenerateMove(*this); }

private:
  GameType gt;
};

コストは仮想関数の使用とほぼ同じですが、ゲームの基本オブジェクトに動的に割り当てられたメモリは不要になり、スタックの割り当ては大幅に高速化されます。

于 2010-09-12T12:46:00.803 に答える
0

これが適切かどうかはよくわかりませんが、 CRTPを介して、元の設計に若干の変更を加えることで静的ポリモーフィズムを実現できる可能性があります。

于 2010-09-12T07:06:42.773 に答える