Есть ли веская причина, почему GCC генерирует прыжок, чтобы перепрыгнуть через одну дешевую инструкцию? - PullRequest
0 голосов
/ 31 августа 2018

Я сравнивал подсчет в циклическом коде. g ++ использовался с кодом -O2, и я заметил, что он имеет некоторые проблемы с перфорированием, когда какое-то условие выполняется в 50% случаев. Я предположил, что это может означать, что код выполняет ненужные переходы (поскольку clang создает более быстрый код, поэтому это не является фундаментальным ограничением).

Что мне показалось смешным в этом выводе asm, так это то, что код перепрыгивает через одно простое добавление.

=> 0x42b46b <benchmark_many_ints()+1659>:       movslq (%rdx),%rax
   0x42b46e <benchmark_many_ints()+1662>:       mov    %rax,%rcx
   0x42b471 <benchmark_many_ints()+1665>:       imul   %r9,%rax
   0x42b475 <benchmark_many_ints()+1669>:       shr    $0xe,%rax
   0x42b479 <benchmark_many_ints()+1673>:       and    $0x1ff,%eax
   0x42b47e <benchmark_many_ints()+1678>:       cmp    (%r10,%rax,4),%ecx
   0x42b482 <benchmark_many_ints()+1682>:       jne    0x42b488 <benchmark_many_ints()+1688>
   0x42b484 <benchmark_many_ints()+1684>:       add    $0x1,%rbx
   0x42b488 <benchmark_many_ints()+1688>:       add    $0x4,%rdx
   0x42b48c <benchmark_many_ints()+1692>:       cmp    %rdx,%r8
   0x42b48f <benchmark_many_ints()+1695>:       jne    0x42b46b <benchmark_many_ints()+1659>

Обратите внимание, что мой вопрос не в том, как исправить мой код, я просто спрашиваю, есть ли причина, по которой хороший компилятор в O2 генерирует инструкцию jne, чтобы перепрыгнуть через 1 дешевую инструкцию. Я спрашиваю, потому что из того, что я понимаю , можно «просто» получить результат сравнения и использовать его, чтобы без скачков увеличить счетчик (rbx в моем примере) на 0 или 1.

редактировать: источник: https://godbolt.org/z/v0Iiv4

1 Ответ

0 голосов
/ 31 августа 2018

Соответствующая часть источника (по ссылке Godbolt в комментарии, которую вы действительно должны отредактировать в своем вопросе):

const auto cnt = std::count_if(lookups.begin(), lookups.end(),[](const auto& val){
    return buckets[hash_val(val)%16] == val;});

Я не проверял заголовки libstdc ++, чтобы увидеть, реализован ли count_if с if() { count++; }, или использует ли он троичный для поддержки кода без ответвлений. Наверное, условно. (Компилятор может выбрать любой, но троичный с большей вероятностью скомпилируется в cmovcc или setcc. Без ветвей).


Похоже, что gcc переоценил стоимость безветвления для этого кода с общей настройкой . -mtune=skylake (подразумевается -march=skylake) дает нам код без ответвлений для этого независимо от -O2 против -O3 или -fno-tree-vectorize против -ftree-vectorize. (На проводнике компилятора Godbolt я также поместил счетчик в отдельную функцию, которая подсчитывает vector<int>&, поэтому нам не нужно разбираться со временем и cout code-gen в main.)

  • код ветвления: gcc8.2 -O2 или -O3 и O2/3 -march=haswell или broadwell
  • код без ответвления: gcc8.2 -O2/3 -march=skylake.

Странно. Код без ответвлений имеет ту же цену, что и Бродвелл против Skylake. Я задавался вопросом, предпочитает ли Скайлэйк против Хэсвелла безветвление из-за более дешевого cmov. Модель внутренних затрат GCC не всегда соответствует инструкциям x86 при ее оптимизации в середине (в GIMPLE, архитектурно-нейтральном представлении). Он еще не знает, какие инструкции x86 будут фактически использоваться для последовательности без ветвей. Так, может быть, задействована операция условного выбора, и gcc моделирует ее как более дорогую на Haswell, где cmov составляет 2 мопа? Но я протестировал -march=broadwell и все еще получил разветвленный код. Надеемся, что мы можем исключить это, предполагая, что стоимостная модель gcc знает, что Broadwell (не Skylake) был первым Uarch семейства Intel P6 / SnB, который имеет однократные операции cmov, adc и sbb (целочисленные операции с 3 входами). ).

Я не знаю, что еще в опции настройки Skylake в gcc, которая делает его предпочтительным для кода без ответвлений для этого цикла. Сбор данных эффективен на Skylake, но gcc выполняет автоматическую векторизацию (с vpgatherqd xmm) даже с -march=haswell, где это не похоже на выигрыш, потому что сбор дорог, и требует 32x64 => 64-битных умножений SIMD, используя 2x vpmuludq на каждый входной вектор. Может быть, стоит с SKL, но я сомневаюсь, HSW. Также, вероятно, пропущенная оптимизация, чтобы не упаковать элементы dword, чтобы собрать вдвое больше элементов с почти одинаковой пропускной способностью для vpgatherdd.

Я исключил, что функция была менее оптимизирована, потому что она называлась main (и помечалась cold). Как правило, рекомендуется не помещать ваши микробенчмарки в main: компиляторы, по крайней мере, используются для оптимизации main по-другому (например, для размера кода, а не просто для скорости).


Clang делает его без ответвлений даже с -O2.


Когда компиляторам приходится выбирать между ветвлением и ветвлением, у них есть эвристика, которая угадывает, что будет лучше . Если они думают, что это очень предсказуемо (например, вероятно, в основном не принято), это склоняется в пользу ветвления.

В этом случае эвристик мог бы решить, что из всех 2 ^ 32 возможных значений для int найти именно то значение, которое вы ищете, редко. ==, возможно, обманули gcc, думая, что это будет предсказуемо.

Иногда ветвление может быть лучше, в зависимости от цикла, поскольку оно может нарушить зависимость от данных. См. флаг оптимизации gcc -O3 делает код медленнее, чем -O2 для случая, когда он был очень предсказуемым, а -O3 код без ветвей был медленнее.

-O3, по крайней мере, раньше был более агрессивным при if-преобразовании условных выражений в последовательности без ветвей, такие как cmp; lea 1(%rbx), %rcx; cmove %rcx, %rbx, или в этом случае более вероятно xor -зеро / cmp / sete / add. (На самом деле gcc -march=skylake использует sete / movzx, что намного хуже.)

Без каких-либо данных профилирования / инструментария во время выполнения эти предположения могут легко оказаться неверными. В таком случае светит оптимизация по профилю . Скомпилируйте с -fprofile-generate, запустите его, затем скомпилируйте с -fprofile-use, и вы, вероятно, получите код без ответвлений.


Кстати, -O3 обычно рекомендуется в эти дни. Опасен ли уровень оптимизации -O3 в g ++? . Он не включает -funroll-loops по умолчанию, поэтому он раздувает код только при автоматической векторизации (особенно с очень большим полностью развернутым скалярным прологом / эпилогом вокруг крошечного SIMD-цикла, который узкие места на контурах. /facepalm.)

...