2015-12-18 3 views
2

Я workon на некоторый код C++, который определяетбезопасно ли вывести __builtin_expect на встроенную функцию?

#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); } 

так как х, как предполагается, является результатом некоторой проверки состояния)

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

+0

@KarolyHorvath: Отредактировано с возможным кодом. – einpoklum

+0

Обратите внимание, что '__builtin_expect' применим только к интегральным типам, поэтому вы можете (и должны) передавать/возвращать значение. – 5gon12eder

+0

@ 5gon12eder: Правильно. – einpoklum

ответ

1

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

+1

Ну, если оптимизация отключена, то это действительно не имеет значения, я имею в виду, что 'LIKELY()' - это всего лишь подсказка по оптимизации. Тем не менее, точка взята. – einpoklum

+0

Но вы не указали, что GCC или clang могут не выполнять встроенные функции с '__attribute __ ((always_inline))' - или даже с помощью 'inline' - когда вложение имеет смысл. Это действительно так? С включенными оптимизациями? – einpoklum

+0

@einpoklum При включении оптимизаций такая короткая функция, скорее всего, всегда будет встроена. – nwellnhof

3

Оставайтесь с проверенными и надежными макросами, даже если мы все знаем, что макросов следует избегать в целом. Функции 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_MAXmargin] (с 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.

enter image description here

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

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

В отличие от этого, здесь есть сюжет на 1-й против 3-го .

enter image description here

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

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

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

Смежные вопросы