Извините за, может быть, слишком абстрактный вопрос, но для меня это довольно практично +, может быть, некоторые эксперты имели подобный опыт и могут его объяснить.
У меня большой код размером около 10000 строк.
Я замечаю, что если в определенном месте я ставлю
if ( expression ) continue;
где выражение всегда ложь (дважды проверено логикой кода и cout), но зависит от неизвестных параметров (поэтому компилятор не может просто избавиться от этой строки во время компиляции) скорость программы увеличился на 25% (результат расчета одинаковый). Если я измеряю скорость самого цикла, коэффициент ускорения больше 3.
Почему это может произойти и каковы возможные способы использования этой возможности ускорения без таких уловок?
Постскриптум Я использую gcc 4.7.3, оптимизацию -O3.
Больше информации:
Я пробовал два разных выражения, оба работают.
Если я изменю строку на:
if ( expression ) { cout << " HELLO " << endl; continue; };
ускорение прошло.
Если я изменю строку на:
expression;
ускорение прошло.
Код, который окружает строку, выглядит так:
for ( int i = a; ; ) {
do {
i += d;
if ( d*i > d*ilast ) break;
// small amount of calculations, and conditional calls of continue;
} while ( expression0 );
if ( d*i > dir*ilast ) break;
if ( expression ) continue;
// very big amount calculations, and conditional calls of continue;
}
цикл for выглядит странно. Это потому, что я изменил петли, чтобы поймать эту горлышко бутылки. Первоначально выражение было равно expression0 и вместо do-loop у меня было только это продолжение.
Я попытался использовать __builtin_expect для того, чтобы понять прогноз ветвления. С
// the expression (= false) is supposed to be true by branch prediction.
if ( __builtin_expect( !!(expression), 1) ) continue;
ускорение составляет 25%.
// the expression (= false) is supposed to be false by branch prediction.
if ( __builtin_expect( !!(expression), 0) ) continue;
ускорение прошло.
Если я использую -O2 вместо -O3, эффект исчезнет. Код немного (~ 3%) медленнее, чем быстрая версия O3 с ложным условием.
То же самое для «-O2 -finline-functions -funswitch-loops -fpredictive-commoning -fgcse-after-reload -ftree-vectorize». С еще одной опцией: «-O2 -finline-functions -funswitch-loop -fpredictive-commoning -fgcse-after-reload -ftree-vectorize -fipa-cp-clone» эффект усиливается. С «линией» скорость такая же, без «линии» код на 75% медленнее.
Причина только в следующем условном операторе. Итак, код выглядит так:
for ( int i = a; ; ) {
// small amount of calculations, and conditional calls of continue;
if ( expression ) continue;
// calculations1
if ( expression2 ) {
// calculations2
}
// very big amount calculations, and conditional calls of continue;
}
Значение expression2 почти всегда ложно. Поэтому я изменил это так:
for ( int i = a; ; ) {
// small amount of calculations, and conditional calls of continue;
// if ( expression ) continue; // don't need this anymore
// calculations1
if ( __builtin_expect( !!(expression2), 0 ) ) { // suppose expression2 == false
// calculations2
}
// very big amount calculations, and conditional calls of continue;
}
И получили желаемое ускорение на 25%. Даже немного больше. И поведение больше не зависит от критической линии.
Если кто-то знает материалы, которые могут объяснить это поведение без догадок, я буду очень рад прочитать и принять их ответ.
Нашел это.
Причина была в следующем условном операторе. Итак, код выглядит так:
for ( int i = a; ; ) {
// small amount of calculations, and conditional calls of continue;
if ( expression ) continue;
// calculations1
if ( expression2 ) {
// calculations2
}
// very big amount calculations, and conditional calls of continue;
}
Значение expression2 почти всегда ложно. Поэтому я изменил это так:
for ( int i = a; ; ) {
// small amount of calculations, and conditional calls of continue;
// if ( expression ) continue; // don't need this anymore
// calculations1
if ( __builtin_expect( !!(expression2), 0 ) ) { // suppose expression2 == false
// calculations2
}
// very big amount calculations, and conditional calls of continue;
}
И получили желаемое ускорение на 25%. Даже немного больше. И поведение больше не зависит от критической линии.
Я не знаю, как это объяснить, и не могу найти достаточно материала по прогнозу отрасли.
Но я думаю, дело в том, что вычисления2 должны быть пропущены, но компилятор не знает об этом и предполагает выражение2 == true по умолчанию.
Между тем предположим, что в простом продолжении проверки
if ( expression ) continue;
выражение == ложно, и приятно пропускает вычисления2, как это должно быть сделано в любом случае.
В случае, когда под, если у нас есть более сложные операции (например, cout), предполагается, что выражение истинно и трюк не работает.
Если кто-то знает материалы, которые могут объяснить это поведение без догадок, я буду очень рад прочитать и принять их ответ.
Внедрение этой недоступной ветви нарушает граф потока. Обычно компилятор знает, что поток выполнения идет от вершины цикла прямо к тесту выхода и снова к началу. Теперь в графе есть дополнительный узел, где поток может выйти из цикла. Теперь нужно по-разному скомпилировать тело цикла, состоящее из двух частей.
Это почти всегда приводит к ухудшению кода. Почему этого здесь нет, я могу предложить только одно предположение: вы не компилировали с профилированием информации. Следовательно, компилятор должен делать предположения. В частности, он должен делать предположения о том, что ветвь будет взята во время выполнения.
Ясно, что, поскольку предположения, которые он должен сделать, различны, вполне возможно, что результирующий код отличается по скорости.
Ненавижу это говорить, но ответ будет довольно техническим и, что более важно, очень специфичным для вашего кода. Настолько, что, вероятно, никто за пределами вас не собирается тратить время на изучение сути вашего вопроса. Как и предполагали другие, вполне возможно, что это будет зависеть от прогнозирования ветвлений и других оптимизаций после компиляции, связанных с конвейерной обработкой.
Единственное, что я могу предложить, чтобы помочь вам сузиться, если это проблема оптимизации компилятора или оптимизации после компиляции (ЦП), это снова скомпилировать ваш код, с -O2
против -O3
, но на этот раз добавьте следующие дополнительные опции: -fverbose-asm -S
, Передайте каждый из выходных данных в два разных файла, а затем запустите что-то вроде sdiff, чтобы сравнить их. Вы должны увидеть много различий.
К сожалению, без хорошего понимания ассемблерного кода, будет сложно разобраться в этом, и, честно говоря, не многие люди в Stack Overflow имеют терпение (или время) потратить на решение этой проблемы более нескольких минут. Если вы не владеете сборкой (предположительно, x86), то я бы предложил найти коллегу или друга, которые помогут вам разобрать вывод сборки.