Есть ли подсказка компилятора для GCC, чтобы заставить предсказание ветвления всегда идти определенным путем?
Для архитектур Intel есть ли способ дать указание компилятору GCC генерировать код, который всегда заставляет предсказывать переходы определенным образом в моем коде? Аппаратное обеспечение Intel даже поддерживает это? А как насчет других компиляторов или аппаратных средств?
Я бы использовал это в коде C++, где я знаю случай, когда я хочу быстро работать, и мне нет дела до замедления, когда нужно использовать другую ветку, даже если она недавно взяла эту ветку.
for (;;) {
if (normal) { // How to tell compiler to always branch predict true value?
doSomethingNormal();
} else {
exceptionalCase();
}
}
Как продолжение вопроса к Евджану Мустафе, может ли подсказка просто указать подсказку в первый раз, когда процессор встречает инструкцию, все последующие предсказания ветвлений функционируют нормально?
7 ответов
Правильный способ определения вероятных / маловероятных макросов в C++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)
теряет контекст для C++11 контекстных преобразований.
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)
просто чтобы облегчить задачу.
Имейте в виду, что:
- это нестандартно
- предиктор веток компилятора / процессора, вероятно, более опытен в принятии таких решений, чем вы, так что это может быть преждевременной микрооптимизацией
У 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 о макросах kernal, которые, вероятно, () и вряд ли () используют эту функцию:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
Обратите внимание !!
в макросе мы можем найти объяснение этому в разделе Почему нужно использовать (условие) вместо (условие)?,
Тот факт, что этот метод используется в ядре Linux, не означает, что его всегда имеет смысл использовать. Из этого вопроса я недавно видел ответ на различие между производительностью функции при передаче параметра в качестве постоянной времени или переменной времени компиляции, что многие методы ручной оптимизации не работают в общем случае. Нам нужно тщательно профилировать код, чтобы понять, эффективна ли методика. Многие старые методы могут даже не соответствовать современным оптимизациям компилятора.
Обратите внимание, что хотя встроенные функции не являются переносимыми, Clang также поддерживает __builtin_expect.
Также на некоторых архитектурах это может не иметь значения.
Нет, нет (По крайней мере, на современных процессорах x86.)
__builtin_expect
упомянутое в других ответах влияет на способ компоновки gcc кода сборки. Он не оказывает прямого влияния на предиктор ветвления ЦП. Конечно, косвенное влияние на предсказание ветвлений будет вызывать переупорядочение кода. Но на современных процессорах x86 нет инструкции, которая говорит CPU "предположить, что эта ветвь занята / не занята".
См. Этот вопрос для получения более подробной информации: Прогнозирование ветви префикса Intel x86 0x2E/0x3E фактически используется?
Чтобы быть понятным, __builtin_expect
и / или использование -fprofile-arcs
может повысить производительность вашего кода, давая подсказки предсказателю ветвления посредством компоновки кода (см. " Оптимизация производительности сборки x86-64 - выравнивание и предсказание ветвления"), а также улучшая поведение кэша, предотвращая "маловероятный" код от "вероятного" код
Поскольку другие ответы все адекватно предложены, вы можете использовать __builtin_expect
дать компилятору подсказку о том, как расположить ассемблерный код. Как отмечают официальные документы, в большинстве случаев встроенный в ваш мозг ассемблер не будет так хорош, как созданный командой GCC. Всегда лучше использовать фактические данные профиля для оптимизации кода, а не гадать.
Аналогичным образом, но пока не упоминается, это специфический для GCC способ заставить компилятор генерировать код по "холодному" пути. Это предполагает использование noinline
а также cold
атрибуты, которые делают именно то, что они звучат, как они делают. Эти атрибуты могут применяться только к функциям, но в C++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
Начиная с C++20 вероятные и маловероятные атрибуты должны быть стандартизированы и уже поддерживаются в g++9. Итак, как обсуждалось здесь, вы можете написать
if (a>b) {
/* code you expect to run often */
[[likely]] /* last statement */
}
например, в следующем коде блок else становится встроенным благодаря [[unlikely]]
в блоке if
int oftendone( int a, int b );
int rarelydone( int a, int b );
int finaltrafo( int );
int divides( int number, int prime ) {
int almostreturnvalue;
if ( ( number % prime ) == 0 ) {
auto k = rarelydone( number, prime );
auto l = rarelydone( number, k );
[[unlikely]] almostreturnvalue = rarelydone( k, l );
} else {
auto a = oftendone( number, prime );
almostreturnvalue = oftendone( a, a );
}
return finaltrafo( almostreturnvalue );
}
__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 имеют инструкции для ветвей, которые, как ожидается, будут приняты, или для ветвей, которые, как ожидается, не будут приняты (есть префикс инструкции; не уверен в деталях). Не уверен, что процессор использует это. Это не очень полезно, потому что предсказание ветвлений справится с этим просто отлично. Так что я не думаю, что вы действительно можете повлиять на прогноз отрасли.
Что касается OP, нет, в GCC нет способа сказать процессору всегда предполагать, что ветвь занята или не занята. У вас есть __builtin_expect, который делает то, что говорят другие. Кроме того, я думаю, что вы не хотите сообщать процессору, занята ли ветка или не всегда. Современные процессоры, такие как архитектура Intel, могут распознавать довольно сложные шаблоны и эффективно адаптироваться.
Однако бывают случаи, когда вы хотите взять на себя управление тем , предсказывается ли ветвь по умолчанию или нет: когда вы знаете, что код будет называться "холодным" в отношении статистики ветвления.
Один конкретный пример: код управления исключениями. По определению код управления будет происходить исключительно, но, возможно, когда это произойдет, желательна максимальная производительность (может быть критическая ошибка, которую необходимо устранить как можно скорее), поэтому вы можете захотеть контролировать прогноз по умолчанию.
Другой пример: вы можете классифицировать ваш ввод и перейти к коду, который обрабатывает результат вашей классификации. Если существует много классификаций, процессор может собирать статистику, но потерять ее, потому что та же классификация не происходит достаточно быстро, а ресурсы прогнозирования выделяются для недавно названного кода. Мне бы хотелось, чтобы примитив сказал процессору "пожалуйста, не выделяйте ресурсы для прогнозирования этого кода", как вы иногда можете сказать "не кэшируйте это".