Вы являетесь жертвой неудачного предсказания ветвлений .
Что такое предсказание отрасли?
Рассмотрим железнодорожный узел:
Изображение Mecanismo, через Wikimedia Commons. Используется по лицензии CC-By-SA 3.0 .
Теперь, ради аргумента, предположим, что это еще в 1800-х годах - до дальней связи или радиосвязи.
Вы оператор развязки и слышите прибывающий поезд. Вы понятия не имеете, в каком направлении это должно идти. Вы останавливаете поезд, чтобы спросить водителя, в каком направлении они хотят. И тогда вы установите переключатель соответствующим образом.
Поезда тяжелые и обладают большой инерцией. Таким образом, им требуется вечность, чтобы начать и замедлить.
Есть ли способ лучше? Вы угадываете, в каком направлении будет идти поезд!
- Если вы угадали, это продолжается.
- Если вы догадались, капитан остановится, отступит и закричит на вас, чтобы щелкнуть выключателем. Затем он может перезапустить другой путь.
Если вы угадаете каждый раз , поезд никогда не остановится.
Если вы слишком часто угадываете неправильно , поезд будет тратить много времени на остановку, резервное копирование и перезапуск.
Рассмотрим оператор if: на уровне процессора это инструкция перехода:
Вы процессор, и вы видите ветку. Вы понятия не имеете, по какому пути это пойдет. Чем ты занимаешься? Вы останавливаете выполнение и ждете, пока предыдущие инструкции не будут выполнены. Затем вы продолжаете идти по правильному пути.
Современные процессоры сложны и имеют длинные трубопроводы. Таким образом они берут навсегда, чтобы "согреться" и "замедлить".
Есть ли способ лучше? Вы угадываете, в каком направлении пойдет филиал!
- Если вы угадали, вы продолжаете выполнять.
- Если вы угадали, вам нужно промыть конвейер и откатиться до ответвления. Затем вы можете перезапустить другой путь.
Если вы каждый раз угадаете , выполнение никогда не остановится.
Если вы слишком часто угадываете неправильно , вы тратите много времени на остановку, откат и перезапуск.
Это прогноз отрасли. Я признаю, что это не лучшая аналогия, так как поезд мог просто сигнализировать направление с флагом. Но в компьютерах процессор не знает, в каком направлении пойдет ветка, до последнего момента.
Итак, как вы могли бы стратегически угадать, сколько раз поезд должен вернуться и пойти по другому пути? Вы смотрите на прошлую историю! Если поезд идет влево 99% времени, значит, вы уехали. Если это чередуется, то вы чередуете свои догадки. Если это происходит в одну сторону каждые три раза, вы догадаетесь, то же самое ...
Другими словами, вы пытаетесь определить шаблон и следовать ему. Это более или менее то, как работают предсказатели ветвлений.
Большинство приложений имеют хорошо управляемые ветви. Таким образом, современные отраслевые предикторы, как правило, достигают> 90% показателей успеха. Но когда они сталкиваются с непредсказуемыми ветвями без распознаваемых шаблонов, предикторы ветвей практически бесполезны.
Дальнейшее чтение: статья "Ветка предиктора" в Википедии .
Как указывалось выше, виновником является следующее if-утверждение:
if (data[c] >= 128)
sum += data[c];
Обратите внимание, что данные равномерно распределены между 0 и 255. Когда данные отсортированы, примерно первая половина итераций не войдет в оператор if. После этого все они введут оператор if.
Это очень удобно для предиктора ветвления, поскольку ветвь последовательно идет в одном и том же направлении много раз. Даже простой насыщающий счетчик будет правильно предсказывать ветвь, за исключением нескольких итераций после переключения направления.
Быстрая визуализация:
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N N N N N ... N N T T T ... T T T ...
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
Однако, когда данные полностью случайны, предиктор ветвления становится бесполезным, потому что он не может предсказать случайные данные. Таким образом, вероятно, будет около 50% неправильного прогноза (не лучше, чем случайное угадывание).
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
Так что же можно сделать?
Если компилятор не может оптимизировать переход в условный ход, вы можете попробовать некоторые методы, если вы готовы пожертвовать удобочитаемостью для производительности.
Заменить:
if (data[c] >= 128)
sum += data[c];
с:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
Это исключает ветвление и заменяет его на некоторые побитовые операции.
(Обратите внимание, что этот хак не является строго эквивалентным исходному оператору if. Но в этом случае он действителен для всех входных значений data[]
.)
Тесты: Core i7 920 @ 3,5 ГГц
C ++ - Visual Studio 2010 - выпуск x64
// Branch - Random
seconds = 11.777
// Branch - Sorted
seconds = 2.352
// Branchless - Random
seconds = 2.564
// Branchless - Sorted
seconds = 2.587
Java - NetBeans 7.1.1 JDK 7 - x64
// Branch - Random
seconds = 10.93293813
// Branch - Sorted
seconds = 5.643797077
// Branchless - Random
seconds = 3.113581453
// Branchless - Sorted
seconds = 3.186068823
Замечания:
- В ветке: между отсортированными и несортированными данными существует огромная разница.
- С помощью Hack: нет разницы между отсортированными и несортированными данными.
- В случае C ++ хак на самом деле медленнее, чем с веткой, когда данные сортируются.
Общее правило - избегать зависимого от данных ветвления в критических циклах (например, в этом примере).
Обновить:
GCC 4.6.1 с -O3
или -ftree-vectorize
на x64 может генерировать условный ход. Таким образом, нет никакой разницы между отсортированными и несортированными данными - оба быстры.
(Или несколько быстро: для уже отсортированного случая cmov
может быть медленнее, особенно если GCC ставит его на критический путь, а не просто add
, особенно на Intel до Broadwell, где cmov
имеет 2 цикла задержки: флаг оптимизации gcc -O3 делает код медленнее, чем -O2 )
VC ++ 2010 не может генерировать условные перемещения для этой ветви даже в /Ox
.
Компилятор Intel C ++ (ICC) 11 совершает чудеса. Он чередует две петли , тем самым поднимая непредсказуемую ветвь на внешнюю петлю. Таким образом, он не только защищен от неправильных прогнозов, но также в два раза быстрее, чем то, что могут генерировать VC ++ и GCC! Другими словами, ICC воспользовалась тестовым циклом, чтобы победить тест ...
Если вы дадите компилятору Intel код без ответвлений, он просто сразу его векторизует ... и так же быстро, как и с ветвью (с циклическим обменом).
Это говорит о том, что даже зрелые современные компиляторы могут сильно различаться по своей способности оптимизировать код ...