Я пытался оптимизировать какой-то чрезвычайно критичный для производительности код (алгоритм быстрой сортировки, который вызывается миллионы и миллионы раз в симуляции Монте-Карло) путем развертывания цикла. Вот внутренний цикл, который я пытаюсь ускорить:
// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}
Я пробовал развернуть что-то вроде:
while(true) {
if(myArray[++index1] < pivot) break;
if(myArray[++index1] < pivot) break;
// More unrolling
}
while(true) {
if(pivot < myArray[--index2]) break;
if(pivot < myArray[--index2]) break;
// More unrolling
}
Это не имело абсолютно никакого значения, поэтому я вернул его в более читаемую форму. У меня был аналогичный опыт, когда я пробовал развернуть цикл. Учитывая качество предикторов ветвления на современном оборудовании, когда, если вообще, разворачивание цикла все еще является полезной оптимизацией?
Ответы:
Развертывание цикла имеет смысл, если вы можете разорвать цепочки зависимостей. Это дает вышедшему из строя или суперскалярному процессору возможность лучше планировать работу и, следовательно, работать быстрее.
Простой пример:
Здесь цепочка зависимостей аргументов очень короткая. Если вы получаете остановку из-за ошибки кеширования в массиве данных, процессор не может ничего делать, кроме как ждать.
С другой стороны, этот код:
мог бежать быстрее. Если вы получаете промах в кэше или другую задержку в одном вычислении, есть еще три других цепочки зависимостей, которые не зависят от остановки. ЦП, вышедший из строя, может их выполнить.
источник
Это не будет иметь никакого значения, потому что вы делаете одинаковое количество сравнений. Вот лучший пример. Вместо того:
записывать:
Даже тогда это почти наверняка не будет иметь значения, но теперь вы выполняете 50 сравнений вместо 200 (представьте, что сравнение более сложное).
Однако ручное развертывание цикла в целом во многом является артефактом истории. Это еще один из постоянно растущего списка вещей, которые хороший компилятор сделает за вас, когда это необходимо. Например, большинство людей не заботятся о том, чтобы писать
x <<= 1
илиx += x
вместоx *= 2
. Вы просто пишете,x *= 2
а компилятор оптимизирует его для вас, как лучше.По сути, становится все меньше и меньше необходимости переоценивать свой компилятор.
источник
Независимо от предсказания ветвления на современном оборудовании, большинство компиляторов все равно разворачивают цикл за вас.
Было бы полезно узнать, сколько оптимизаций делает за вас ваш компилятор.
Я нашел презентацию Феликса фон Лейтнера очень поучительной по этому вопросу. Рекомендую прочитать. Резюме: Современные компиляторы ОЧЕНЬ умны, поэтому ручная оптимизация почти никогда не бывает эффективной.
источник
Насколько я понимаю, современные компиляторы уже разворачивают циклы там, где это необходимо - примером является gcc, если переданы флаги оптимизации, о которых говорится в руководстве, он будет:
Итак, на практике вполне вероятно, что ваш компилятор выполнит тривиальные дела за вас. Поэтому вы должны убедиться, что как можно больше ваших циклов позволяет компилятору легко определить, сколько итераций потребуется.
источник
Развертывание цикла, будь то развертывание вручную или развертывание компилятора, часто может быть контрпродуктивным, особенно с более новыми процессорами x86 (Core 2, Core i7). Итог: сравните свой код с развертыванием цикла и без него на любых процессорах, на которых вы планируете развернуть этот код.
источник
Пытаться, не зная, - это не способ сделать это.
Эта сортировка занимает много времени?
Все, что делает разворачивание цикла, - это уменьшение накладных расходов цикла на увеличение / уменьшение, сравнение для условия остановки и прыжки. Если то, что вы делаете в цикле, требует больше циклов инструкций, чем накладные расходы самого цикла, вы не увидите значительного улучшения в процентном отношении.
Вот пример того, как добиться максимальной производительности.
источник
В определенных случаях может быть полезно разворачивание петли. Единственное преимущество - не пропуск некоторых тестов!
Например, он может позволить скалярную замену, эффективную вставку программной предварительной выборки ... Вы будете удивлены, насколько это может быть полезно (вы можете легко получить 10% ускорение в большинстве циклов даже с -O3), агрессивно разворачивая.
Как было сказано ранее, это во многом зависит от цикла и компилятора, и эксперимент необходим. Трудно составить правило (или эвристика компилятора для развертывания была бы идеальной)
источник
Раскрутка петли полностью зависит от размера вашей задачи. Это полностью зависит от того, сможет ли ваш алгоритм сократить размер на более мелкие группы работы. То, что вы сделали выше, на это не похоже. Я не уверен, можно ли вообще развернуть симуляцию Монте-Карло.
Хорошим сценарием для развертывания цикла было бы вращение изображения. Так как можно было чередовать отдельные группы работ. Чтобы это сработало, вам нужно уменьшить количество итераций.
источник
Развертывание цикла по-прежнему полезно, если в цикле и вместе с ним много локальных переменных. Для повторного использования этих регистров вместо сохранения одного для индекса цикла.
В вашем примере вы используете небольшое количество локальных переменных, не злоупотребляя регистрами.
Сравнение (до конца цикла) также является серьезным недостатком, если сравнение тяжелое (то есть не связанное с
test
инструкциями), особенно если оно зависит от внешней функции.Развертывание цикла также помогает повысить осведомленность ЦП о предсказании ветвлений, но это все равно происходит.
источник