Comments 9
Статья не про это, но всё, сразу хочется спросить, почему не вынесли умножение на i, как общую часть, вверх? Тогда уже не надо делать доп. рассчёт массива, а иметь статический массив {1.5,2}. Или компилятор достаточно умный, и в данном случае сам это делает? Так же, сразу хочется какую-то мат. функцию заиспользовать для выбора между 1.5 и 2, чтобы вообще не иметь массивы, которые часто превращаются в обращение к памяти, что дорого.
При исследованиях производительности я бы начал с ассемблерного листинга. Вполне допускаю вероятность что в варианте с if-ами мы можем получить avx с масочками вместо бранчей. При условии правильно подобранных флагов разумеется.
О чем-то похожем недавно рассказывал небезызвестный Matt Godbolt - создатель Complier explorer:
Отличный кейс про то, как легко измерить не алгоритм, а поведение измерительного контура.
Фактически benchmark обучил предсказатель переходов и создал иллюзию «выигрыша» ветвления на малых данных. Самое ценное в статье — не branch predictor, а методологический поворот: сомнение не в коде, а в эксперименте.
Хорошее напоминание, что повторяемость входных данных превращает бенчмарк в эхо-камеру, а «красивые цифры» — в ловушку.
Там на самом деле есть что поисследовать, если подойти дотошно. Во-первых надо обязательно заглянуть в листинг ассемблера (при этом разные компиляторы могут выдавать сильно разный результат), а ещё лучше весь тест вообще на асме написать, контролируемо провоцируя попадания или промахи предсказателя. Замерять производительность лучше через RDTMC или вообще через RDPMC, там можно будет посмотреть отношение количества инструкций процессора на количество тактов. Хорошо установить также маску, чтобы не давать потоку прыгать с ядра на ядро. Через RDPMC можно (вроде бы) также получить количество промахов предсказателя. Результат может также зависеть от архитектуры. На интеловских камушках промах предсказателя "стоит" вроде бы 15-20 тактов (оно в общем экспериментально измеряемо, но надо повозиться). Ещё при разных объёмах данных надо смотреть, укладываемся ли мы в кэш или нет. По моему скромному опыту на реальных задачах современные компиляторы достаточно неплохо оптимизируют сами по себе, так что переписывание кода на "без переходов" обычно не даёт такого уж большого выигрыша.
Цель статьи - показать, что сравнить две функции на скорость не так просто, как кажется. Что существуют неочевидные ловушки, которые могут исказить результат в разы. Для этого вывода не нужен ни ассемблер, ни RDPMC, ни привязка к ядрам.
Для того, чтобы отличить вилку от ложки не нужен электронный микроскоп, достаточно попробовать поесть суп.
Это понятно, что скорость выполнения и замеров может зависеть от входных данных, как в смысле размера, так и содержимого, но, скажем так - у меня есть некоторые сомнения в том, что предсказатель переходов способен удержать в памяти 4К итераций, там буфер вроде сильно меньше, так что тут действительно надо быть крайне осторожным и не спешить с выводами. Я на досуге покопаю поглубже, у меня только пара вопросов - какой компилятор использовался и на каком камушке производились замеры? Также для чистоты эксперимента было бы здорово увидеть полный код, а не только отдельные функции.
Ловушка профилирования кода