Неделя оптимизатора
Что-то у меня эта неделя проходит под знаком единства и борьбы с оптимизатором компилятора. То я компилятор чем удивлю, то он меня в ответ.
Рисую шаблонный движок для реализации конечных автоматов (Finite state machine). Такое я уже делал раньше, просто сейчас свистелки с перделками нужны оказались совсем другие. Еще было странное желание, чтобы движок мог компилироваться для AVR, Ардуино то бишь. Забегая вперед скажу, что таки сделал в лучшем виде.
В общем, в одном месте у меня получился такой код
typedef void (*stateHandler)();
stateHandler currentState;
// Irrelevant crap skipped
void SetNewState(stateHandler* _newState)
{
if (currentState != _newState)
{
callOnExitFor(currentState);
currentState = _newState;
callOnEnterFor(_newState);
}
}
Настоящий код выглядит совсем не так. Потому что на шаблонах. Но суть от этого не меняется, кроме того, тащить в этот псто всю шаблонную магию мне природная скромность не позволяет. Код позволяет определить набор состояний, каждое из которых обслуживается своей функцией и потом совершать переходы из состояния в состояние путем вызова SetNewState. Собственно состояние определяется функцией- обработчиком этого состояния.
SetNewState проверяет, не переходим ли в то же состояние. Для КА подобные переходы вполне легальны, и даже обычны, хотя и не имеют большого смысла. Задача SetNewState, кроме регистрации следующего состояния, заключается еще и в том, чтобы вызвать функцию-эпилог для прошлого состояния и пролог для нового. Соответственно if нужен для того, чтобы не вызывать эпилог и пролог в случае, когда переходим в то же самое состояние.
Как водится, написал юнит-тест. Набор состояний с функциями-обработчиками, таблица переходов, все дела. Поскольку это юнит-тест, все функции-обработчики оказались одинаковыми и внутри просто увеличивали счетчик вызовов да указатель на себя записывали в переменную, которую я потом ассертом проверял:
stateHandler test_lastState;
int test_Counter;
void InitState()
{
test_lastState = InitState;
++test_Counter;
}
void RunState()
{
test_lastState = RunState;
++test_Counter
}
И так далее. Юнит-тест тривиальнейший, вызываем переход, проверяем, что новое состояние установлено согласно ожидаемому да счетчик вызовов увеличивается правильно.
Потом настала очередь проверить работу обработчиков эпилога и пролога. И вот тут ко мне пришел великий птиц обломинго. При переходе из InitState в RunState ни эпилог, ни пролог не вызывались. По свежему опыту проверил поведение debug сборки и убедился, что чертовщина происходит только в релизе. Было очевидно, что опять что-то соптимизировалось не так и не туда.
Грабли нашлись после взятия всех анализов у оператора if из функции SetNewState. В моем юнит-тесте при переходе из InitState в RunState условие InitState != RunState почему-то всегда вычислялось как false. Оказалось, что в релизной сборке InitState == RunState. Оптимизатор линкера просто взял и слил две одинаковые функции в одну, поскольку посчитал, что они делают абсолютно одно и то же. Откуда ж ему было знать, болезному, что я ожидал, что указатели на InitState и RunState должны различаться?
В итоге движок КА оказался конкретно так сконфужен. И, поскольку в моем случае вероятность того, что в продакшен коде тоже могут потребоваться обработчики разных состояний с абсолютно одинаковым кодом внутри, очень велика, пришлось искать вариант, на который оптимизация компилятора повлиять бы не смогла. В итоге нашел замечательный со всех сторон способ убрать проверки из рантайма и заставить компилятор их сделать во время компиляции.
Но об этом потом.


