Pull to refresh

Учимся правильно бенчмаркать 2: как компилятор бьет в спину

Reading time5 min
Views1.7K
Получить годные цифры бенчмарка это полдела, однако вторая половина их правильно интерпретировать, узнать что-то новое, и суметь применить. 100x отличия промеж дебажным и нормальным билдом удивили, решил копнуть глубже. По итогам получше узнал, что происходит в дебаге; поискал отличия между 2005 и 2008 студией (не нашел); выяснил, как ускорить дебажный билд в 3 раза за пару минут (ставим блок против удара в спину); методом «взять и запустить» получил результаты, отличающиеся от авторских в 3.5 раза (адская сила x64 в действии!); и для смеха замерил плохой, негодный недовектор против хорошего (плохой оказался до 100 раз быстрее). Подробности под катом.

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

Ликвидировав дрожание, вернулся к изначальному вопросу: тормоза в 100 с лишним раз это очень медленно, откуда они берутся. Чужой тест хорошо, но свой привычнее, написал, запустил. Создавать проект было лениво, скомпилировать с комстроки куда быстрее.

cl2005 /O2 /EHsc 1.cpp
std it++ res=49995000, 28.5 msec
std ++it res=49995000, 28.6 msec
my res=49995000, 19.0 msec

cl2005 /EHsc 1.cpp
std it++ res=49995000, 534.2 msec
std ++it res=49995000, 437.6 msec
my res=49995000, 69.9 msec


Упс. Результаты однако отличаются: у тех парней в 30 раз медленнее, у меня всего в 10. У тех парней постинкремент тормозит в 3 раза, у меня примерно на 20%. Что я делаю не так? Неужели настолько лютые отличия компилятора и забандленной библиотеки? Ну, щаз поставим 2008, проверим.

cl2008 /O2 /EHsc 1.cpp
std it++ res=49995000, 64.3 msec
std ++it res=49995000, 63.4 msec
my res=49995000, 19.0 msec

cl2008 /EHsc 1.cpp
std it++ res=49995000, 732.1 msec
std ++it res=49995000, 678.8 msec
my res=49995000, 70.0 msec


Мдя. Отличия действительно есть, только в обратную сторону: релизный билд VS 2005 однако был втрое быстрее. Улучшения налицо; видимо, SCL в студии номер 2008 стал втрое секьюрнее, тк. при _SECURE_SCL 0 скорость выходит одинаковая. (Забегая вперед, на большинстве других тестов тоже практически одинаковая.) Что еще я делаю не так? Компилятор теперь одинаковый, std::vector вроде как тоже, вывод однозначный: неправильно компилирую. В смысле, расходятся ключики компилятора. Смотрим в солюшн, там довольно развесистая комстрока.

/Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Gm /EHsc /RTC1 /MDd /Fo"Debug\\" /Fd"Debug\vc90.pdb" /W3 /nologo /c /ZI /TP /errorReport:prompt


Революционным чутьем и перебором удается неважные ключи быстро выкинуть, а важные оставить. Важных ключей получается три штуки: а) /MDd, б) /RTC1, в) /ZI. Результаты выглядит так.

cl2008 /Od /EHsc /MDd 1.cpp
std it++ res=49995000, 6026.1 msec
std ++it res=49995000, 1953.9 msec
my res=49995000, 66.5 msec

cl2008 /Od /EHsc /MDd /RTC1 1.cpp
std it++ res=49995000, 7572.0 msec
std ++it res=49995000, 2385.3 msec
my res=49995000, 101.8 msec

cl2008 /Od /EHsc /MDd /RTC1 /ZI 1.cpp
std it++ res=49995000, 18722.0 msec
std ++it res=49995000, 7131.8 msec
my res=49995000, 511.9 msec


Вот, теперь все как у людей: и обход вектора тормозит как падла, и постинкремент начал тормозить. Ништяк! Можно разбираться, в чем дело. За пару минут чтения cl /? выясняются три простые понятные вещи (по одной на ключик), которые все равно новые (см. хорошо забытые старые) и потому удивительные.

/MDd подлинковал отладочную библиотеку (что неважно) и автоматом включил /D _DEBUG (что важно). От этого SCL включил внутри себя дополнительную пачку проверок вдобавок к тем, которые включаются по _SECURE_SCL, чем успешно затормозил постинкремент в 8 раз, а преинкремент в жалких 3 раза.

/RTC1 включает проверки на срыв стека и неинициализированные переменные. Если прошагать в отладчике дизасм, видно, что во внутреннем цикле появилcя пяток вызовов разных функций (++, !=, * и два деструктора). Каждый вызов необходимо проверить: вдруг он как возьмет и как сорвет стек! Для этого в стек забивается чуть менее 256 байт маркерного добра на каждый (каждый) вызов того или иного перегруженного в STL оператора. Пока-пока, еще 20% и так уже не самой топовой производительности.

/ZI впрочем бьет всех козырем и уносит банк домой. Это Edit and Continue. Исправить и перезапустить программу на лету, наверное, удобно (я сам не знаю, я не пользуюсь). За удобства надо платить и цена составляет еще 3 раза тормозов.

Итого в дебаге постинкремент тормозит в эпичные 291 раз (!!!) по сравнению с релизом. Что немедленно вызвает вопрос: почему у меня в целых 291 раз, а у тех парней только 95?! Собираю оригинальный тест, дождаться конца не хватает терпения, снижаю Count в 10 раз, умножаю в голове время на 10. Выходит, что

it++, x86, release: 1.00
++it, x86, release: 1.00
it++, x86, debug: 275.7
++it, x86, debug: 101.9

it++, x64, release: 0.87779
++it, x64, release: 0.87753
it++, x64, debug: 83.2849
++it, x64, debug: 27.1557


Судя по цифрам, в релизе с моего x86 взяли подоходный налог в размере 13%, а в дебаге вообще ограбили, оставив только трусы, носки и тапочки. Исчезающе обидно, ну да ничего. Все равно XP не брошу, для меня семерка сложно, слишком аэро прозрачный, плюс второй ждем сервис пак.

По результатам ясно, кто виноват в адских тормозах отладочного билда. 2 с небольшим раза жрет _SECURE_SCL, 5 раз отключение оптимизации, от 3 раз (++it) до 8 раз (it++) жрут проверки под _DEBUG, 1.2 раза набрасывает /RTC, и финальным аккордом еще до 3 раз набрасывает /ZI, итого 2*5*8*1.2*3 = 288, курочка по зернышку, весь код в… проверках, тормозища в 300 (триста) раз. Курицу руками, конечно же, едят. Из извечных трех теперь остается только один вопрос: что делать?

В целом делать можно много чего, но долго. Однако за 2 минуты можно сделать 2 полезных вещи. Во1х, если принять волевое решением не пользоваться Edit and Continue (это если оно на вашем проекте вообще работает) и генерить обычные советские PDB, оно начинает работать в 3 раза быстрее, если не все 5 раз. Во2х, /RTC проверки отключать, конечно, некруто. Однако! Чтение MSDN выявляет интересную прагму runtime_checks. Те. можно отключить эти проверки точечно для особо частых функций в проекте, или там для всего STL. Обставляем секцию внешних include двумя строчками, наслаждаемся.

#pragma runtime_checks("",off)
...
#pragma runtime_checks("",restore)


Получаем обратно свои честные 6 сек вместо 18 сек при использовании it++, 2 сек вместо 7 сек при ++it. Вывод про необходимость использовать ++it убедительно подтвержден. Дебажный билд ускорен примерно в 3 раза. Профит! Пробуем применить прознанное к исходному тесту, получается аналогично. По меньшей мере, на старом добром x86.

vanilla
it++, x86, debug: 275.7
++it, x86, debug: 101.9

#pragma runtime_checks, /Zi instead of /ZI
it++, x86, debug: 102.2
++it, x86, debug: 30.0


Плохой, кривой, негодный и примитивный недовектор, который не умеет вообще ничего, показывает в дебаге свои честные 0.066 сек, те. в 90 раз быстрее варианта it++, или 30 раз варианта ++it. Про 100 раз в начале поста я наврал округлил. Проверка у него только одна: корректности индекса при доступе (что, впрочем, покрывает примерно 99% нужных от вектора проверок); фокус с пре- и постинкрементами отсутствуют вместе с итераторами. Это ничего не значит. Автор ни на что не намекает. Бенчмарк явно синтетический. Вектор написан буквально за 3 минуты и функционал соответствующий, те. по сравнению с STL не существует. Скорость отладочного билда необязательно важна; лишняя проверка может быть важна; скорость разработки важна всем и всегда; истину завсегда подскажет профайлер.

Помните про _SECURE_SCL, про _DEBUG, про ключики компилятора, про #pragma runtime_checks. Эффект, мнэээ, бывает ошеломляющий. (Меня лично отличие в 300 раз по итогам убило. А отличие от 3 до 5 раз из-за /ZI вместо /Zi после этого еще и съело труп. «Я об этом знал, но не догадывался.»)

Правильных вам бенчмарков. И быстрой отладки.
Tags:
Hubs:
Total votes 76: ↑74 and ↓2+72
Comments24

Articles