Есть ли подсказка компилятора для GCC, чтобы заставить предсказание ветвей всегда идти определенным образом?

Для архитектуры Intel существует ли способ дать указание компилятору GCC генерировать код, который всегда форсирует предсказание ветвлений определенным образом в моем коде? Поддерживает ли это оборудование Intel? Как насчет других компиляторов или аппаратных средств?

Я бы использовал это в коде на С++, где я знаю случай, который я хочу запустить быстро, и не заботятся о замедлении, когда нужно использовать другую ветку, даже когда она недавно взяла эту ветвь.

for (;;) {
  if (normal) { // How to tell compiler to always branch predict true value?
    doSomethingNormal();
  } else {
    exceptionalCase();
  }
}

Как следует по вопросу для Евджана Мустафы, может ли подсказка просто указать подсказку в первый раз, когда процессор встречает команду, все последующие предсказания ветвления, нормально функционирующие?

Ответ 1

Правильный способ определения вероятных/маловероятных макросов в С++ 11 выглядит следующим образом:

#define LIKELY(condition) __builtin_expect(static_cast<bool>(condition), 1)
#define UNLIKELY(condition) __builtin_expect(static_cast<bool>(condition), 0)

Если эти макросы определены следующим образом:

#define LIKELY(condition) __builtin_expect(!!(condition), 1)

Это может изменить смысл операторов if и разбить код. Рассмотрим следующий код:

#include <iostream>

struct A
{
    explicit operator bool() const { return true; }
    operator int() const { return 0; }
};

#define LIKELY(condition) __builtin_expect((condition), 1)

int main() {
    A a;
    if(a)
        std::cout << "if(a) is true\n";
    if(LIKELY(a))
        std::cout << "if(LIKELY(a)) is true\n";
    else
        std::cout << "if(LIKELY(a)) is false\n";
}

И его вывод:

if(a) is true
if(LIKELY(a)) is false

Как вы можете видеть, определение LIKELY с использованием !! в качестве приведения к bool ломает семантику if.

Дело здесь не в том, что operator int() и operator bool() должны быть связаны. Это хорошая практика.

Скорее, использование !!(x) вместо static_cast<bool>(x) теряет контекст для контекстных конверсий С++ 11.

Ответ 2

GCC поддерживает функцию __builtin_expect(long exp, long c), чтобы обеспечить такую ​​функцию. Вы можете проверить документацию здесь.

Где exp - это используемое условие, а c - ожидаемое значение. Например, в вашем случае вы хотели бы

if (__builtin_expect(normal, 1))

Из-за неудобного синтаксиса это обычно используется при определении двух настраиваемых макросов типа

#define likely(x)    __builtin_expect (!!(x), 1)
#define unlikely(x)  __builtin_expect (!!(x), 0)

просто для облегчения задачи.

Учтите, что:

  • это не стандартный
  • предиктор отрасли компилятора/процессора, вероятно, более квалифицирован, чем вы, при решении таких задач, поэтому это может быть преждевременная микро-оптимизация

Ответ 3

gcc имеет long __builtin_expect (long exp, long c) (внимание мое):

Вы можете использовать __builtin_expect для предоставления компилятору ветки информация прогнозирования. В общем, вы должны предпочесть использовать фактические обратная связь профиля для этого (-fprofile-arcs), поскольку программисты заведомо плохо предсказав, как их программы фактически выполняют. Однако есть приложения, в которых эти данные трудно собрать.

Возвращаемое значение - это значение exp, которое должно быть интегральным выражение. Семантика встроенных заключается в том, что ожидается, что exp == c. Например:

if (__builtin_expect (x, 0))
   foo ();

указывает, что мы не ожидаем вызова foo, так как мы ожидаем, что x будет нуль. Поскольку вы ограничены интегральными выражениями для exp, вы следует использовать такие конструкции, как

if (__builtin_expect (ptr != NULL, 1))
   foo (*ptr);

при тестировании указателей или значений с плавающей запятой.

В качестве примечаний к документации вы должны использовать фактическую обратную связь с профилем и в этой статье приведен практический пример этого и как это делается в их случае по крайней мере, становится улучшением по сравнению с использованием __builtin_expect. Также см. Как использовать профилированные оптимизации в g++?.

Мы также можем найти статью о новинках ядра Linux на макросах ядра, вероятно() и маловероятных(), которые используют эту функцию:

#define likely(x)       __builtin_expect(!!(x), 1)
#define unlikely(x)     __builtin_expect(!!(x), 0)

Обратите внимание на !!, используемый в макросе, мы можем найти объяснение этого в Зачем использовать!! (условие) вместо (условие)?.

Просто потому, что этот метод используется в ядре Linux, не означает, что всегда имеет смысл его использовать. Мы можем видеть из этого вопроса, что недавно я ответил на разницу между производительностью функции при передаче параметра как постоянной времени компиляции или переменной, что многие методы оптимизации ручного проката не работают в общем дело. Нам нужно тщательно прорисовывать код, чтобы понять, эффективна ли методика. Многие старые методы могут даже не иметь отношения к современной оптимизации компилятора.

Обратите внимание, что встроенные функции не переносятся clang также поддерживает __builtin_expect.

Также на некоторых архитектурах это может не повлиять.

Ответ 4

Нет, нет. (По крайней мере, на современных процессорах x86.)

__builtin_expect, упомянутый в других ответах, влияет на то, как gcc упорядочивает код сборки. Это не влияет непосредственно на предсказатель ветвления процессора. Конечно, будут косвенные эффекты на предсказание ветвей, вызванные переупорядочением кода. Но на современных процессорах x86 нет инструкции, которая сообщает процессору: "Предположим, что эта ветка/не берется".

См. этот вопрос для получения более подробной информации: Intel x86 0x2E/0x3E Префикс Branch Prediction фактически используется?

Чтобы быть ясным, __builtin_expect и/или использование -fprofile-arcs может улучшить производительность вашего кода, как путем предоставления намеков на предсказатель ветвления с помощью макета кода (см. Оптимизация производительности сборки x86-64 - Выравнивание и прогнозирование ветвей), а также улучшение поведения кэша, сохраняя "маловероятный" код от "вероятного" кода.

Ответ 5

Как и все остальные ответы, вы можете использовать __builtin_expect, чтобы дать компилятору подсказку о том, как организовать ассемблерный код. Как отмечают официальные документы, в большинстве случаев сборщик, встроенный в ваш мозг, не будет таким же хорошим, как тот, который был создан командой GCC. Всегда лучше использовать фактические данные профиля, чтобы оптимизировать ваш код, а не гадать.

Вдоль аналогичных строк, но еще не упомянутых, является специфическим для GCC способом заставить компилятор генерировать код на "холодном" пути. Это связано с использованием атрибутов noinline и cold, которые делают именно то, что они звучат так, как они. Эти атрибуты могут применяться только к функциям, но с С++ 11 вы можете объявлять встроенные лямбда-функции, и эти два атрибута могут также применяться к лямбда-функциям.

Хотя это все еще относится к общей категории микро-оптимизации, и, таким образом, применяются стандартные рекомендации - тест не угадывает - я чувствую, что он более полезен, чем __builtin_expect. Вряд ли любые поколения процессоров x86 используют подсказки предсказания ветвления (ссылка), поэтому единственное, что вы можете повлиять на все равно, - это порядок кода сборки, Поскольку вы знаете, что такое обработчик ошибок или код "крайний регистр", вы можете использовать эту аннотацию, чтобы гарантировать, что компилятор никогда не будет прогнозировать ветку и не свяжет ее с "горячим" кодом при оптимизации размера.

Использование образца:

void FooTheBar(void* pFoo)
{
    if (pFoo == nullptr)
    {
        // Oh no! A null pointer is an error, but maybe this is a public-facing
        // function, so we have to be prepared for anything. Yet, we don't want
        // the error-handling code to fill up the instruction cache, so we will
        // force it out-of-line and onto a "cold" path.
        [&]() __attribute__((noinline,cold)) {
            HandleError(...);
        }();
    }

    // Do normal stuff
    ⋮
}

Еще лучше, GCC автоматически игнорирует это в пользу обратной связи профиля, когда он доступен (например, при компиляции с -fprofile-use).

См. официальную документацию здесь: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes

Ответ 6

__ builtin_expect может использоваться, чтобы сообщить компилятору, каким образом вы ожидаете перехода ветки. Это может повлиять на то, как генерируется код. Типичные процессоры быстрее запускают код. Поэтому, если вы пишете

if (__builtin_expect (x == 0, 0)) ++count;
if (__builtin_expect (y == 0, 0)) ++count;
if (__builtin_expect (z == 0, 0)) ++count;

компилятор будет генерировать код типа

if (x == 0) goto if1;
back1: if (y == 0) goto if2;
back2: if (z == 0) goto if3;
back3: ;
...
if1: ++count; goto back1;
if2: ++count; goto back2;
if3: ++count; goto back3;

Если ваш намек верен, это приведет к выполнению кода без каких-либо фактически выполненных ветвей. Он будет работать быстрее, чем обычная последовательность, где каждый оператор if будет разворачиваться вокруг условного кода и будет выполнять три ветки.

Новые процессоры x86 имеют инструкции для ветвей, которые, как ожидается, будут приняты, или для ветвей, которые, как ожидается, не будут приняты (там есть префикс инструкции, не уверены в деталях). Не уверен, использует ли процессор. Это не очень полезно, потому что предсказание ветвей справится с этим просто отлично. Поэтому я не думаю, что вы действительно можете повлиять на предсказание ветвей.

Ответ 7

Что касается OP, нет, в GCC нет способа сказать процессору, чтобы всегда считать, что ветка выполнена или не принимается. У вас есть __builtin_expect, который делает то, что говорят другие. Кроме того, я думаю, вы не хотите говорить процессору, берется ли ветка или не всегда. Сегодня процессоры, такие как архитектура Intel, могут распознавать довольно сложные шаблоны и эффективно адаптироваться.

Однако есть моменты, когда вы хотите взять контроль над тем, по умолчанию ли предсказана ветка или нет: когда вы знаете, что код будет называться "холодным" по отношению к статистике ветвления.

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

Другой пример: вы можете классифицировать свой ввод и переходить в код, который обрабатывает результат вашей классификации. Если существует много классификаций, процессор может собирать статистику, но терять их, потому что одна и та же классификация не происходит достаточно быстро, а ресурсы прогнозирования посвящены недавно названному коду. Я бы хотел, чтобы был примитив, чтобы сообщить процессору "пожалуйста, не посвящайте ресурсы прогнозирования этому коду", как вы иногда можете сказать "не кешируйте это".