C#でステートマシンを実装するのに最適な(パフォーマンスが重要な場合)方法は何ですか?
-
06-07-2019 - |
質問
次のオプションを思いつきました:
gotoステートメントの使用:
Start:
goto Data
Data:
goto Finish
Finish:
;
switchステートメントの使用:
switch(m_state) {
case State.Start:
m_state = State.Data;
break;
case State.Data:
m_state = State.Finish;
break;
case State.Finish:
break;
}
gotoを使用して一緒に切り替える:
switch(m_state) {
case State.Start:
goto case State.Data2;
case State.Data1:
goto case State.Finish;
case State.Data2:
m_state = State.Data1;
//call to a function outside the state machine
//that could possibly change the state
break;
case State.Finish:
break;
}
gotoステートメントを使用した最初のオプションは、より高速で冗長性が少ないため好まれます。しかし、それが最良の選択肢かどうかはわかりません。賢明なパフォーマンスかもしれませんが、読みやすさに関してはわかりません。だから私はこの質問をします。どのオプションを好むのですか?
解決
gotoを超えるスイッチの利点は、命令ポインタだけでなく、変数に状態があることです。
gotoメソッドでは、ステートマシンが他のすべてを制御するメインループである必要があります。ステートを失うため、ステップマシンから抜け出すことはできません。
switchメソッドを使用すると、ステートマシンが分離され、外部からイベントを処理する任意の場所に移動できます。ステートマシンに戻ると、yuuが中断したところから継続します。 gotoバージョンでは不可能な、複数のステートマシンを並べて実行することもできます。
3番目の選択肢をどこで使用するのかわかりません。これは、最初の選択肢のように見えますが、その周りに無駄なスイッチがあります。
他のヒント
相互呼び出し/再帰関数が好きです。あなたの例を適応するには:
returnvalue Start() {
return Data();
}
returnvalue Data() {
return Finish();
}
returnvalue Finish() {
…
}
理論的には、コンパイラの出力が goto
ソリューションと同等になるように、この を完全にインライン化できます(したがって、同じ速度)。現実的には、 C#コンパイラ/ JITterはおそらく勝ちません’それ。しかし、ソリューションは非常に読みやすいので(まあ、IMHO)、非常に慎重なベンチマークが 条件が実際に劣っていることを証明した後、 goto
ソリューションに置き換えますまたは、スタックオーバーフローが発生します(この単純なソリューションではなく、より大きなオートマトンがこの問題に遭遇します)。
それでも、 goto case
ソリューションに固執する 。どうして?乱雑な goto
パスタ全体がブロック構造( switch
ブロック)の中によく入れられており、スパゲッティが残りのコードを壊さないため、ボロネーゼ。
結論:機能のバリエーションは明確ですが、一般的に問題が発生しやすいです。 goto
ソリューションは面倒です。 goto case
のみが、中間的なクリーンで効率的なソリューションを提供します。パフォーマンスが本当に重要な場合(およびオートマトンがボトルネックである場合)、構造化された goto case
バリアントを選択します。
4番目のオプションがあります。
イテレータを使用して、ステートマシンを実装します。 素敵な短い記事で方法を説明
ただし、いくつかの欠点があります。反復子の外部から状態を操作することはできません。
それが非常に速いかどうかもわかりません。ただし、いつでもテストを実行できます。
ステートマシンの遷移ロジックを個別の機能に分割したい場合は、switchステートメントを使用してのみ実行できます。
switch(m_state) {
case State.Start:
m_state = State.Data;
break;
case State.Data:
m_state = ComputeNextState();
break;
case State.Finish:
break;
}
読みやすく、またswitchステートメントのオーバーヘッド(Gotoに対して)は、まれな状況でのみパフォーマンスの違いを生じます。
編集:
「goto case」を使用できますパフォーマンスを少し改善するには:
switch(m_state) {
case State.Start:
m_state = State.Data; // Don't forget this line!
goto case State.Data;
case State.Data:
m_state = ComputeNextState();
break;
case State.Finish:
break;
}
ただし、状態変数の更新を忘れる危険があります。後で微妙なバグが発生する可能性があります(「m_state」が設定されていると仮定したため)、回避することをお勧めします。
個人的には、gotoを使用した2番目の方法の方が好きです。最初の方法では、新しい状態に移行するために(たとえば)不要なループステップが必要になるためです