безопасно ли передавать __builtin_expect встроенной функции?

Я работаю над кодом C ++, который определяет

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

и мне было интересно — почему не встроенная функция? то есть почему нет

template <typename T> inline T likely(T x) { return __builtin_expect((x), 1); }

(или, может быть

inline int likely(int x) { return __builtin_expect((x), 1); }

так как x должен быть результатом некоторой проверки состояния)

Макрос и функция должны делать то же самое, верно? Но потом я задумался: может быть, это из-за __builtin_expect… может ли быть так, что внутри встроенной вспомогательной функции все работает иначе?

2

Решение

Оставайтесь с проверенными и проверенными макросами, даже если мы все знаем, что макросов вообще следует избегать. inline функции просто не работают. В качестве альтернативы — особенно если вы используете GCC — забудьте __builtin_expect в целом и используйте вместо этого профильную оптимизацию (PGO) с фактическими данными профилирования.

__builtin_expect Это особенность в том, что он на самом деле ничего не «делает», а просто намекает компилятору на то, какая ветвь, скорее всего, будет принята. Если вы используете встроенный в контексте, который не является условием ветвления, компилятор должен будет распространять эту информацию вместе со значением. Интуитивно я ожидал, что это произойдет. Интересно, что документация НКУ а также лязг не очень ясно об этом. Тем не менее, мои эксперименты показывают, что Clang явно не распространяет эту информацию. Что касается GCC, мне все еще нужно найти программу, в которой на самом деле уделяется внимание встроенному, поэтому я не могу сказать наверняка. (Или, другими словами, это все равно не имеет значения.)

Я проверил следующую функцию.

std::size_t
do_computation(std::vector<int>& numbers,
const int base_threshold,
const int margin,
std::mt19937& rndeng,
std::size_t *const hitsptr)
{
assert(base_threshold >= margin && base_threshold <= INT_MAX - margin);
assert(margin > 0);
benchmark::clobber_memory(numbers.data());
const auto jitter = make_jitter(margin - 1, rndeng);
const auto threshold = base_threshold + jitter;
auto count = std::size_t {};
for (auto& x : numbers)
{
if (LIKELY(x > threshold))
{
++count;
}
else
{
x += (1 - (x & 2));
}
}
benchmark::clobber_memory(numbers.data());
// My benchmarking framework swallows the return value so this trick with
// the pointer was needed to get out the result.  It should have no effect
// on the measurement.
if (hitsptr != nullptr)
*hitsptr += count;
return count;
}

make_jitter просто returns случайное целое число в диапазоне [-м, м] где м это его первый аргумент.

int
make_jitter(const int margin, std::mt19937& rndeng)
{
auto rnddist = std::uniform_int_distribution<int> {-margin, margin};
return rnddist(rndeng);
}

benchmark::clobber_memory это неоперация, которая запрещает компилятору оптимизировать изменения данных вектора. Это реализовано так.

inline void
clobber_memory(void *const p) noexcept
{
asm volatile ("" : : "rm"(p) : "memory");
}

Декларация do_computation был аннотирован __attribute__ ((hot)), Оказалось, что это влияет на то, сколько оптимизаций применяет компилятор. много.

Код для do_computation был создан так, что любая ветвь имела сравнимую стоимость, что несколько увеличивало стоимость для случая, когда ожидание не было выполнено. Также было удостоверено, что компилятор не будет генерировать векторизованный цикл, для которого ветвление будет несущественным.

Для сравнения, вектор numbers 100000000 случайных целых чисел из диапазона [0, INT_MAX] и случайный base_threshold сформировать интервал [0, INT_MAXmargin] (с margin установлен на 100) был создан с недетерминированным генератором псевдослучайных чисел. do_computation(numbers, base_threshold, margin, …) (составляется в отдельном модуле перевода) вызывается четыре раза и измеряется время выполнения для каждого прогона. Результат первого запуска был отброшен для устранения эффектов холодного кэша. Среднее и стандартное отклонение оставшихся прогонов было построено в зависимости от частоты попаданий (относительной частоты, с которой LIKELY аннотация была правильной). «Джиттер» был добавлен, чтобы сделать результат четырех запусков не одинаковым (в противном случае я бы боялся слишком умных компиляторов), сохраняя при этом частоту обращений по существу фиксированной. 100 точек данных были собраны таким образом.

Я скомпилировал три разные версии программы с GCC 5.3.0 и Clang 3.7.0, передав им -DNDEBUG, -O3 а также -std=c++14 флаги. Версии отличаются только способом LIKELY определено.

// 1st version
#define LIKELY(X) static_cast<bool>(X)

// 2nd version
#define LIKELY(X) __builtin_expect(static_cast<bool>(X), true)

// 3rd version
inline bool
LIKELY(const bool x) noexcept
{
return __builtin_expect(x, true);
}

Хотя концептуально три разные версии, я сравнил 1улица против 2й и 1улица против 3й. Данные за 1улица поэтому был по существу собран дважды. 2й и 3й упоминаются как «намекнул»
на участках.

Горизонтальная ось следующих графиков показывает коэффициент попадания для LIKELY аннотация и вертикальная ось показывает усредненное время процессора на одну итерацию цикла.

Вот сюжет на 1улица против 2й.

введите описание изображения здесь

Как видите, GCC эффективно игнорирует подсказку, создавая одинаково эффективный код независимо от того, была ли дана подсказка или нет. Сланг, с другой стороны, явно обращает внимание на подсказку. Если частота попаданий падает на низкое значение (т. Е. Подсказка была неправильной), код подвергается штрафу, но для высоких показателей совпадения (т. Е. Подсказка была хорошей) код превосходит код, сгенерированный GCC.

В случае, если вы задаетесь вопросом о характере кривой в форме холма: это аппаратный предсказатель ветвления в действии! Это не имеет ничего общего с компилятором. Также обратите внимание, что этот эффект полностью затмевает эффекты __builtin_expect, что может быть причиной, чтобы не беспокоиться об этом.

Напротив, вот сюжет на 1улица против 3й.

введите описание изображения здесь

Оба компилятора выдают код, который по сути работает одинаково. Для GCC это мало что говорит, но что касается Clang, то __builtin_expect похоже, не учитывается, когда обернута в функцию, которая делает его свободным от GCC для всех уровней попаданий.

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

Я понимаю, что это был не ваш вопрос, поэтому я буду кратким, но в целом предпочитаю собирать фактические данные профилирования, а не угадывать вероятные ветви вручную. Данные будут более точными, и GCC будет уделять им больше внимания.

4

Другие решения

Не гарантируется, что компилятор встроит встроенную функцию. Большинство современных компиляторов относятся к inline Ключевое слово только в качестве подсказки. Если вы заставляете встраивание, используя __attribute__((always_inline)) с GCC (или __forceinline с MSVC), не должно иметь значения, используете ли вы встроенную функцию или макрос (но даже __forceinline может не работать). В противном случае возможно, что функция не будет встроенной. Например, GCC не встроенные функции с оптимизацией выключен. В этом случае результирующий код будет значительно медленнее. Я бы придерживался макроса, чтобы быть в безопасности.

1

По вопросам рекламы [email protected]