Каков наилучший (когда важна производительность) способ реализации конечного автомата в 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:
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 конечный автомат изолирован, и вы можете отправиться куда угодно для обработки событий извне. Когда вы возвращаетесь к конечному автомату, он просто продолжается там, где вы остановились. Вы даже можете иметь более одного конечного автомата, работающего бок о бок, что невозможно в версии goto.
Я не уверен, куда вы идете с третьей альтернативой, она выглядит как первая альтернатива с бесполезным переключателем вокруг нее.
Другие советы
Я предпочитаю взаимно вызывающие / рекурсивные функции. Чтобы адаптировать ваш пример:
returnvalue Start() {
return Data();
}
returnvalue Data() {
return Finish();
}
returnvalue Finish() {
…
}
Теоретически, это может быть полностью встроенным, так что вывод компилятора эквивалентен вашему решению goto
(следовательно, такая же скорость). Реально, компилятор C # / JITter, вероятно, выиграл & # 8217; это . Но так как решение намного более читабельно (ну, 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, так как первый потребует ненужного шага цикла (например), чтобы перейти в новое состояние