Неделя оптимизатора

Что-то у меня эта неделя проходит под знаком единства и борьбы с оптимизатором компилятора. То я компилятор чем удивлю, то он меня в ответ.

Рисую шаблонный движок для реализации конечных автоматов (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 должны различаться?

В итоге движок КА оказался конкретно так сконфужен. И, поскольку в моем случае вероятность того, что в продакшен коде тоже могут потребоваться обработчики разных состояний с абсолютно одинаковым кодом внутри, очень велика, пришлось искать вариант, на который оптимизация компилятора повлиять бы не смогла. В итоге нашел замечательный со всех сторон способ убрать проверки из рантайма и заставить компилятор их сделать во время компиляции.

Но об этом потом.

Пятницо, да!

А я поимел дофига фана на работе.

Сижу, пишу эмулятор сетевого девайса. Даже не девайса, а целой PLC. Хорошо так пишу, натыкаюсь на место, где нужно некий кусок данных зазиповать перед выдачей клиенту. Piece of cake – качаю zlib1.dll с официального сайта, подключаю, пишу йунит-тесты. Все работает аж фуфайко заворачиваеццо.

А потом дернул меня чОрд собрать йунит-тест в релизе. Впрочем, чего это он меня дернул, я обычно все сразу собираю в релизе, а на дебаг переключаюсь только в крайнем случае, когда без дебаггера никуда. Так что вопрос скорее в том, какого лешего я вообще собирал дебаг версию тестов? Впрочем, это неважно, собираю релиз версию юнит-теста, а оно обламывается. У мну йунит-тесты прогоняются как post-build step, соответственно если юнит-тест обламыается, то и билд фейлится.

На этот раз фейл оказался эпичным. Из тех йунит-тестов, которые проверяли zlib функциональность вылезает хаааароший такой Access violation во всю морду при первом же вызове функции из zlib1.dll Что характерно – эти же тесты в дебаге отработали как надо.

Дальше чудесатее и странноватее. В своем коде перетряхнул все, что можно, и что нельзя тоже. Проверил и перепроверил все настройки. Потом еще раз. Затем стал отлаживать disassembly. Обнаружил забавную вещь – вызов zlib функции выглядел как call any_random_address. Запахло расстрелом памяти и прочими heap corruption.

Вынос всего лишнего, в том числе и того, что даже теоретически не могло стрелять по памяти, на картину не повлияло. Debug же сборка продолжала угорать надо мной, выдавая 100% pass безо всяких спецэффектов.

Так бы и не понял, чезанах, если бы не случай.

Препарируя пациента дебаггером в стопицотый раз, обратил внимание, что zlib1.dll отсутствует в списке загруженных модулей. Интересно, что дебаг сборка библиотеку исправно грузила. Релизная же нет – тестовый экзешник запускался даже на машине, куда ни одна версия zlib1.dll еще не добралась. Запускался и валился по AV.

Ага, сказали суровые сибирские мужики. Никаким late binding и LoadLibrary там и не пахло, экзешник динамически линковал zlib1.dll. Только линковал как-то странно.

В итоге, проблема оказалась в том, что все ссылки на импортированные из dll функции линкер зачем-то заботливо потер. В итоге библиотека оказалась как бы слинкована, но ни одна функция из нее в экзешник не импортировалась. Вместо этого импортированные функции указывали куда придется. Интересно, что отключение оптимизации в линковщике (/OPT:ref) проблему убирало (поэтому и дебаг сборка работала, кстати). Но такой вариант меня не устраивал по вполне понятным причинам и мне пришлось искать альтернативные решения.

К слову, в списках рассылки zlib мне удалось найти пару упоминаний о такой проблеме. Проблема проявлялась как раз на такой же версии, как и у меня. Все сошлись во мнении, что линкер офигевал от чего-то, что попало в .lib файл библиотеки, который кто-то заботливо собрал каким то специальным, только одному ему известным способом.

Короче, проблема решилась самостоятельной сборкой zlib из исходников старым добрым компилятором 2005 студии.

Мораль всего этого такова – пишите йунит-тесты вообще для всего, @#$%!!!!. Мне страшно представить объем попаболи, если бы этот бабах случился бы в готовом продукте.

И еще один вопрос остается открытым – КАК?

Открытие дня

Только что с прискорбием узнал, что Build Events в Visual Studio не агрегируются и поэтому не наследуются из Property Sheets. Потомушта это “simple property, not the aggregate one” (пруф).

Пичаль.

РабочеЭ

Написал юнит-тест.

В последней строчке его короткое слово FAIL().

Ибо нефиг.

История языков программирования

Краткая, неполная и практически полностью неправильная. Здесь.

1972 – Dennis Ritchie invents a powerful gun that shoots both forward and backward simultaneously. Not satisfied with the number of deaths and permanent maimings from that invention he invents C and Unix.

Хозяйке на заметку

Как в большой западной компании безошибочно определить соотечественника?

По синим таблицам Far Manager на экране, как же еще.

У непосвященных местных эти таблицы вызывают нечто вроде пещерного ужаса. У посвященных, но ниасиливших, кстати, возникает недоумение “как они ЭТИМ пользуются”. Асиливших пока не встречал.

Злободненвое

О работе, а не о том, о чем все подумали.

Иногда мне кажется, что дядька, рисующий Дилберта, незаметно сидит за соседним с моим столом

Dilbert.com

Сипласпласное

Сегодня провели с коллегой полдня в пространных рассуждениях о многопоточности и с чем ее едят.

Езь код

std::map<int, bool> myMap;

// Map is filled in with some data here

void SetValue(int key, bool newValue)
{
myMap[key] = newValue;
}

Небольшие ограничения: мап заполняется значениями только один раз, при запуске программы, гарантированно однопоточно. После инициализации SetValue может вызываться из разных потоков для разных ключей (то есть гонок по одному ключу быть не может). Кроме того, вызов SetValue с ключом, которого в мапе нет, исключен (т.е. невозможен).

С моей точки зрения, с данными допущениями код SetValue можно считать потокобезопасным (присвоение bool считаем атомарной операцией, не углубляясь особенно в детали). Конечно, во всех этих допущениях просвечивает некоторое джидайство (к примеру, operator[] у мапа вообще-то для вставки используется), но если их принять, то никакого криминала в подобном грубом обращении с этим STL контейнером не будет.

Моя аргументация тут такова – SetValue() модифицирует только значения, не изменяя состояние контейнера, поэтому для обеспечения потокобезопасности кода в данном конкретном случае необходимо и достаточно обеспечить потокобезопасность оператора присвоения для типа значения. Контр-аргумент коллеги тоже имеет под собой все основания, ведь нигде прямо не написано, что присвоение элементу мапа нового значения – операция потокобезопасное. Я парировал тем, что в MSDN четко описаны случаи инвалидации итераторов, в число которых изменение уже хранимого значения без вставки или удаления нового ключа никаким боком не входит.

Впрочем, мы джедаи опытные и на личности не переходим. Но в итоге трехчасовой беседы с отсылкой к исходникам STL в коде SetValue появился таки появился лок с моим каментом “Меня заставили”.

Я не отрицаю, что в общем случае так делать не надо. Бо и правда, первый же залетевший дятел завтра заменит мап на какую другую неведому зверюшку и все развалится при первом же неосторожном чихе. Или какой юный падаван проигнорирует жЫрные ворнинги в каментах (а ворнинги там знатные – я, в отличие от, код без комментариев не пишу) выкорчует код и без изменений применит там, где не надо, в результате чего и огребет как тумаков от обладателей синего меча, так и поджопников – от владельцев красного, плюс еще щелбанов от юзверей на закуску.

Но в том-то и дело, что в данном конкретном случае речь шла не об абстрактном алгоритме в вакууме, а о весьма конкретном случае, когда мы уже под микроскопом рассматривали логи профайлера в надежде выжать из системы еще кусочек перформанса.

Вопрос  – я и правда провафлил какое-нибудь условие или мне стоило стоять на своем до конца?

Для тех, кто еще не забыл схемотехнику

Обходя интереты, нашел интересное

Вот такой вот безтрансформаторный БП.

Уже неделю не могу найти ни одной причины, по которой оно работать не будет. Все вроде бы очень просто – суровые сибирские мужики заряжают 16-вольтовый электролит (C1) прямо из розетки огрызками полупериодов. Однако сама идея заряжать электролитический конденсатор прямо из розетки вызывает у меня труднопреодолимое желание забраться под плинтус и не вылезать оттудова.

Очень, очень полезный сайт

Is my credit card stolen?

И главное, совершенно бесплатно. ;)

9 visitors online now
9 guests, 0 members
Max visitors today: 15 at 09:10 am MST
This month: 23 at 05-03-2012 08:23 am MST
This year: 29 at 01-23-2012 02:50 am MST
All time: 45 at 02-23-2011 09:11 am MST