Оставайтесь с проверенными и надежными макросами, даже если мы все знаем, что макросов следует избегать в целом. Функции inline
просто не работают. В качестве альтернативы - особенно если вы используете GCC - забудьте полностью __builtin_expect
и используйте оптимизацию на основе профиля (PGO) с фактическими данными профилирования.
__builtin_expect
весьма специфичен тем, что на самом деле он ничего не делает, а просто намекает на компилятор относительно того, какая ветка, скорее всего, будет принята. Если вы используете встроенный в контексте, который не является условием ветвления, компилятор должен будет распространять эту информацию вместе со значением. Интуитивно я ожидал, что это произойдет. Интересно, что документация GCC и Clang не очень подробно об этом. Однако мои эксперименты показывают, что 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
просто return
ев случайное число в диапазоне [− м, м], где м является его первым аргументом.
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
был создан таким образом, чтобы любая из ветвей имела сопоставимую стоимость, предоставляя немного большую стоимость случаю, когда ожидания не были выполнены. Он также удостоверился, что компилятор не будет генерировать векторизованный цикл, для которого разветвление будет несущественным.
Для ориентира, вектор 000 случайных чисел из диапазона [0, INT_MAX
] и случайной base_threshold
образуют интервал [0, INT_MAX
− margin
] (с margin
набор 100) был создан с генератором псевдослучайных чисел без детерминированности. do_computation(numbers, base_threshold, margin, …)
(скомпилированный в отдельной единицы перевода) был вызван четыре раза и измерено время выполнения для каждого прогона. Результат первого запуска был отброшен для устранения эффектов холодного кэша. Среднее и стандартное отклонение оставшихся прогонов было построено по отношению к скорости удара (относительная частота, с которой была сделана аннотация LIKELY
). «Джиттер» был добавлен, чтобы результат четырех прогонов не совпал (в противном случае я боялся слишком умных компиляторов), сохраняя при этом фиксированные ставки. Таким образом было собрано 100 точек данных.
Я собрал три различные версии программы как с GCC 5.3.0 и 3.7.0 Clang передавая им -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 st были собраны в основном дважды. 2 nd и 3 rd упоминаются как «намек» на участках.
Горизонтальная ось следующих графиков показывает скорость удара для аннотации LIKELY
, а на вертикальной оси показано усредненное время ЦП на итерацию цикла.
Здесь находится график 1 st по сравнению с 2 0d.
Как вы можете видеть, GCC эффективно игнорирует подсказку, производя одинаково исполнительский код независимо был ли намек дан или нет. С другой стороны, Кланг явно обращает внимание на намек. Если скорость снижения падает (т. Е. Подсказка была неправильной), код наказывается, но для высоких коэффициентов попадания (т. Е. Подсказка была хорошей) код превосходит тот, который генерируется GCC.
В случае, если вам интересно узнать о форме кривой в виде холма: это предсказатель ветвления оборудования на работе! Это не имеет никакого отношения к компилятору. Также обратите внимание, как этот эффект полностью затмевает эффекты __builtin_expect
, что может быть причиной того, что он не слишком беспокоится об этом.
В отличие от этого, здесь есть сюжет на 1-й против 3-го .
Оба Составители производят код, который по существу выполняет равны. Для GCC это не говорит многого, но что касается Clang, то __builtin_expect
, похоже, не учитывается при завершении функции, которая делает ее непригодной для GCC для всех коэффициентов попадания.
Итак, в заключение не используйте функции в качестве оберток. Если макрос написан правильно, это не опасно. (Помимо загрязнения пространства имен.) __builtin_expect
уже ведет себя (по крайней мере, в отношении оценки его аргументов) как функция. Обнаружение вызова функции в макросе не вызывает неожиданных последствий для оценки его аргумента.
Я понимаю, что это был не ваш вопрос, поэтому я буду держать его коротким, но в целом предпочитаю собирать фактические данные профилирования по угадыванию вероятных ветвей вручную. Данные будут более точными, и GCC будет уделять этому больше внимания.
@KarolyHorvath: Отредактировано с возможным кодом. – einpoklum
Обратите внимание, что '__builtin_expect' применим только к интегральным типам, поэтому вы можете (и должны) передавать/возвращать значение. – 5gon12eder
@ 5gon12eder: Правильно. – einpoklum