Какие методы избежать условного разветвления вы знаете?

Иногда цикл, в котором процессор проводит большую часть времени, очень часто пропускает предсказание ветвления (неверное предсказание) (около .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% -ное улучшение в прогнозировании отрасли, вы, вероятно, можете опубликовать статью. Если вам интересно, есть гора бумаг на эту тему.

Вам лучше беспокоиться о хитах и ​​пропущенных кешках.