Какие методы избежать условного разветвления вы знаете?
Иногда цикл, в котором процессор проводит большую часть времени, очень часто пропускает предсказание ветвления (неверное предсказание) (около .5 вероятности). Я видел несколько методов для очень изолированных потоков, но никогда не список. Те, которые, как я знаю, уже исправляют ситуации, когда условие можно переключить на bool и что 0/1 используется каким-то образом для изменения. Есть ли другие условные ветки, которых можно избежать?
например. (Псевдокод)
loop () {
if (in[i] < C )
out[o++] = in[i++]
...
}
Можно переписать, возможно, потерять некоторую удобочитаемость, с чем-то вроде этого:
loop() {
out[o] = in[i] // copy anyway, just don't increment
inc = in[i] < C // increment counters? (0 or 1)
o += inc
i += inc
}
Также я видел методы в дикой смене &&
- &
в условном в определенных контекстах, которые сейчас исчезают. Я новичок на этом уровне оптимизации, но он уверен, что там должно быть больше.
Ответы
Ответ 1
Я считаю, что наиболее распространенный способ избежать ветвления - использовать бит parallelism для уменьшения общего количества переходов, присутствующих в вашем коде. Чем длиннее основные блоки, тем реже трубопровод размывается.
Как уже упоминал кто-то другой, если вы хотите делать больше, чем разворачивать циклы, а также давать подсказки для отрасли, вам нужно будет перейти в сборку. Конечно, это должно быть сделано с предельной осторожностью: ваш типичный компилятор может писать лучше в большинстве случаев, чем человек. Ваша лучшая надежда состоит в том, чтобы сбрить грубые края и сделать предположения, которые компилятор не может вывести.
Здесь приведен пример следующего кода C:
if (b > a) b = a;
В сборке без каких-либо переходов, используя бит-манипуляцию (и крайний комментарий):
sub eax, ebx ; = a - b
sbb edx, edx ; = (b > a) ? 0xFFFFFFFF : 0
and edx, eax ; = (b > a) ? a - b : 0
add ebx, edx ; b = (b > a) ? b + (a - b) : b + 0
Обратите внимание, что в то время как условные ходы сразу подпрыгивают энтузиастами по сборке, это только потому, что они легко понятны и обеспечивают концепцию языка более высокого уровня в удобной единой инструкции. Они не обязательно быстрее, недоступны на старых процессорах, и, сопоставляя ваш код C в соответствующие условные инструкции перемещения, вы просто выполняете работу компилятора.
Ответ 2
Использование примера Matt Joiner:
if (b > a) b = a;
Вы также можете сделать следующее, без необходимости копать код сборки:
bool if_else = b > a;
b = a * if_else + b * !if_else;
Ответ 3
Обобщение приведенного вами примера - "заменить условную оценку математикой"; условно-отраслевое избегание в значительной степени сводится к этому.
Что происходит с заменой &&
на &
, так это то, что, поскольку &&
является короткозамкнутым, он сам по себе является условной оценкой. &
получает те же логические результаты, если обе стороны равны 0 или 1 и не являются короткозамкнутыми. То же самое относится к ||
и |
, за исключением того, что вам не нужно проверять, чтобы стороны были ограничены 0 или 1 (опять же, только для логических целей, т.е. Вы используете результат только Booleanly).
Ответ 4
GCC уже достаточно умен, чтобы заменить условные выражения более простыми инструкциями. Например, более новые процессоры Intel обеспечивают cmov (условное перемещение). Если вы можете использовать его, SSE2 предоставляет некоторые инструкции сравнить 4 целых числа (или 8 шортов или 16 символов) за раз.
Дополнительно, чтобы вычислить минимум, который вы можете использовать (см. эти магические трюки):
min(x, y) = x+(((y-x)>>(WORDBITS-1))&(y-x))
Однако обратите внимание на такие вещи, как:
c[i][j] = min(c[i][j], c[i][k] + c[j][k]); // from Floyd-Warshal algorithm
даже никакие прыжки не подразумеваются намного медленнее, чем
int tmp = c[i][k] + c[j][k];
if (tmp < c[i][j])
c[i][j] = tmp;
Лучше всего предположить, что в первом фрагменте вы чаще всего загрязняете кеш, а во втором - нет.
Ответ 5
На этом уровне все зависит от оборудования и зависит от компилятора. Является ли ваш компилятор достаточно умным для компиляции < без контроля потока? gcc на x86 достаточно умен; lcc - нет. В старых или встроенных наборах инструкций может быть невозможно вычислить < без потока управления.
Помимо этого предупреждения, подобного Кассандре, трудно сделать какие-либо полезные общие утверждения. Итак, вот некоторые общие утверждения, которые могут быть бесполезными:
-
Современное оборудование для прогнозирования ветвей ужасно хорошо. Если бы вы могли найти настоящую программу, где предсказание плохой ветки стоило бы более 1% -2% -ного замедления, я был бы очень удивлен.
-
Обязательные счетчики производительности или другие инструменты, которые расскажут вам, где найти неверные прогнозы отрасли.
-
Если вам действительно нужно улучшить такой код, я бы посмотрел на планирование трассировки и разворот цикла:
-
Loop unrolling реплицирует тела контуров и дает вашему оптимизатору больше потока управления для работы.
-
Расписание трассировки определяет, какие пути, скорее всего, будут приняты, и среди других трюков, он может настраивать направления ветвлений, чтобы оборудование для прогнозирования ветвей работало лучше на наиболее распространенных путях. При развернутых циклах есть все больше и больше путей, поэтому планировщик трассировки имеет больше возможностей для работы с
-
Я был бы против, пытаясь закодировать это сам в сборке. Когда следующий чип выходит с новым оборудованием предсказания ветвей, шансы превосходны, что вся ваша напряженная работа идет вниз. Вместо этого я бы поискал оптимизированный с обратной связью компилятор.
Ответ 6
На мой взгляд, если вы достигнете этого уровня оптимизации, возможно, наступит время перейти на ассемблерный язык.
По сути, вы рассчитываете на компилятор, создающий конкретный шаблон сборки, чтобы использовать эту оптимизацию в C в любом случае. Трудно догадаться, какой именно код компилятор собирается генерировать, поэтому вам придется смотреть на него в любое время, когда будет сделано небольшое изменение - почему бы просто не сделать это в сборке и не сделать с ним?
Ответ 7
Расширение метода, продемонстрированного в исходном вопросе, применяется, когда вам нужно сделать несколько вложенных тестов, чтобы получить ответ. Вы можете создать небольшую битовую маску из результатов всех тестов и "найти" ответ в таблице.
if (a) {
if (b) {
result = q;
} else {
result = r;
}
} else {
if (b) {
result = s;
} else {
result = t;
}
}
Если a и b почти случайны (например, из произвольных данных), и это находится в плотном цикле, то неудачи предсказания ветвления могут действительно замедлить это. Может быть написано как:
// assuming a and b are bools and thus exactly 0 or 1 ...
static const table[] = { t, s, r, q };
unsigned index = (a << 1) | b;
result = table[index];
Вы можете обобщить это на несколько условностей. Я видел, как это было сделано для 4. Если вложенность становится настолько глубокой, вы хотите убедиться, что тестирование всех из них действительно быстрее, чем выполнение минимальных тестов, предложенных методом короткого замыкания.
Ответ 8
Этот уровень оптимизации вряд ли будет иметь значительную разницу во всех, кроме самых горячих горячих точек. Предполагая, что это так (без доказательства в конкретном случае), является формой угадывания, и первое правило оптимизации не действует на догадки.
Ответ 9
Большинство процессоров обеспечивают прогнозирование отрасли, которое составляет более 50%. На самом деле, если вы получите 1% -ное улучшение в прогнозировании отрасли, вы, вероятно, можете опубликовать статью. Если вам интересно, есть гора бумаг на эту тему.
Вам лучше беспокоиться о хитах и пропущенных кешках.