Pull to refresh

Учимся правильно бенчмаркать (в том числе итераторы)

Reading time 3 min
Views 2.5K
Скачал пример из предыдущего постинга, от запуска к запуску время дрожало до 1.5 раз, от 0.76 до 1.09 секунд. Как можно оценивать результаты подобных бенчмарков, неясно. Проблема знакомая, столкнулся и решал буквально вчера. Вкратце, виноват CPU throttling, а так же странный affinity в коде. Под катом борьба (успешная) и обсуждение.

Итак, предыдущий пример про итераторы, VS 2005, 4 прогона. Машина абсолютно незагружена, торрентов итп фоном не запущено, даже винамп выключен. (Хотя, кстати, для CPU bound workloads наличие винампа и даже торрентов особого лишнего дрожания не создает, максимум 1-2%.) Результаты ходят от 0.758 до 1.085, это почти 1.5 раза. На разных прогонах опять же побеждает разное, что для гонки недопустимо. ;)

x = 3256681784 iterator++. Total time : 0.795557
x = 3256681784 ++iterator. Total time : 0.892076

x = 3256681784 iterator++. Total time : 1.08741
x = 3256681784 ++iterator. Total time : 1.0848

x = 3256681784 iterator++. Total time : 0.898355
x = 3256681784 ++iterator. Total time : 0.758123

x = 3256681784 iterator++. Total time : 0.906159
x = 3256681784 ++iterator. Total time : 0.861794


В чем дело? Смотрим в код, там QPC, а перед ним SetThreadAffinityMask. Измена, в штабе не наши. Процессор C2D, он умеет держать разные частоты на разных ядрах. И в момент простоя ОС этим пользуется и частоту снижает. Если откалибровать таймер (QueryPerformanceFrequency) на одном ядре, а данные счетчика прочитать (QueryPerformanceCounter) на другом, либо на этом же, но после повышения частоты, будет мусор.

Меняем ровно один символ, суем в SetThreadAffinityMask вторым параметром 1, а не 0.

x = 3256681784 iterator++. Total time : 0.751778
x = 3256681784 ++iterator. Total time : 0.685859

x = 3256681784 iterator++. Total time : 0.737615
x = 3256681784 ++iterator. Total time : 0.686026

x = 3256681784 iterator++. Total time : 0.736503
x = 3256681784 ++iterator. Total time : 0.688713

x = 3256681784 iterator++. Total time : 0.772983
x = 3256681784 ++iterator. Total time : 0.68895


Много лучше. Первый тест дрожит от 0.736 до 0.772, те. на 5%, а не 50%. Второй от 0.686 до 0.689, те. на 0.4%. Откуда такая разница?

Гипотеза. К моменту запуска второга теста нагревается кеш. Читаем код внимательно. Однако там 5000 прогонов, холодный кеш что-нибудь поменяет только для первого. Гипотеза неверна, отставить.

Гипотеза. Первый тест он первый. Возможно, повышение частоты ядра происходит в его процессе. Что же, давайте погреем процессор перед всеми тестами. Поскольку компилятор очень умный и норовит предрассчитать и заоптимизить константы, особенно которые не используются, вспоминаем про волшебный модификатор volatile. Ребилд, упс, не помогло. Вспоминаем про аффинити. Прибиваем к одному ядру вообще весь процесс, а не просто кусок, который таймим (иначе есть опасность нагреть не то ядро). Ребилд, упс, победа! Итого суем в самое начало программы такие 4 строчки.

	SetProcessAffinityMask(GetCurrentProcess(), 1);
	volatile int zomg = 1;
	for ( int i=1; i<1000000000; i++ )
		zomg *= i;


И наслаждаемся результатом.

x = 3256681784 iterator++. Total time : 0.687585
x = 3256681784 ++iterator. Total time : 0.685685

x = 3256681784 iterator++. Total time : 0.687524
x = 3256681784 ++iterator. Total time : 0.68579

x = 3256681784 iterator++. Total time : 0.686004
x = 3256681784 ++iterator. Total time : 0.688326

x = 3256681784 iterator++. Total time : 0.688472
x = 3256681784 ++iterator. Total time : 0.685775


Бинго. Дрожание менее 1%, что вполне нормально. И теперь отличий, наконец, нет. Как, собственно, и должно быть по теории. (В релизе оба итератора должны развернуться в pointer walk; в дебаге со включенным SECURE_SCL, разумеется в кромешный ад с элементами израиля.)

На линуксе бывает ровно такая же проблема, называется performance governor. Переключение governor на что-нибудь вроде performance, а так же игры с аффинити итп разогрев тоже помогают.

Правильных вам бенчмарков.
Tags:
Hubs:
+79
Comments 21
Comments Comments 21

Articles