Как работают вероятные / маловероятные макросы в ядре Linux и в чем их польза?

я копался в некоторых частях ядра Linux и нашел такие вызовы:

if (unlikely(fd < 0))
{
    /* Do something */
}

или

if (likely(!err))
{
    /* Do something */
}

Я нашел их определение:

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

Я знаю, что они предназначены для оптимизации, но как они работают? И сколько снижения производительности / размера можно ожидать от их использования? И стоит ли хлопот (и, вероятно, потеря переносимости), по крайней мере, в коде узкого места (в пространстве пользователей, конечно).

10 ответов


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

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


это макросы, которые дают подсказки компилятору о том, каким образом может идти ветвь. Макросы расширяются до определенных расширений GCC, если они доступны.

GCC использует их для оптимизации прогнозирования ветвей. Например, если у вас есть что-то вроде следующего

if (unlikely(x)) {
  dosomething();
}

return x;

затем он может реструктурировать этот код, чтобы быть чем-то вроде:

if (!x) {
  return x;
}

dosomething();
return x;

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

большинство современных процессоров теперь имеют какое-то предсказание ветви, но это помогает только тогда, когда вы были через ветвь раньше, и ветвь все еще находится в кэше предсказания ветви.

существует целый ряд других стратегий, которые компилятор и процессор можно использовать в этих сценариях. Вы можете найти более подробную информацию о том, как предсказатели ветвей в Википедии:http://en.wikipedia.org/wiki/Branch_predictor


давайте декомпилируем, чтобы увидеть, что GCC 4.8 делает с ним

без __builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        printf("%d\n", i);
    puts("a");
    return 0;
}

компилировать и декомпилировать с помощью GCC 4.8.2 x86_64 Linux:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

выход:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    x1,%edx
  15:       be 00 00 00 00          mov    x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    x8,%rsp
  34:       c3                      retq

порядок инструкций в памяти не изменился: сначала printf а то puts и retq вернуться.

С __builtin_expect

заменить if (i) с:

if (__builtin_expect(i, 0))

и мы получаем:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    x8,%rsp
  20:       c3                      retq
  21:       ba 01 00 00 00          mov    x1,%edx
  26:       be 00 00 00 00          mov    x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

на printf (составлен в __printf_chk) был перемещен в самый конец функции, после puts и возвращение для улучшения прогнозирования ветвей, как упоминалось в других ответах.

таким образом, это в основном то же самое, что:

int i = !time(NULL);
if (i)
    goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;

эта оптимизация не была выполнена с -O0.

но удачи в написании примера, который работает быстрее с __builtin_expect чем без, процессоры действительно умные в те дни. Мои наивные попытки здесь.


Они заставляют компилятор выдавать соответствующие подсказки ветви, где оборудование поддерживает их. Обычно это означает просто несколько битов в коде инструкции, поэтому размер кода не изменится. Процессор начнет получать инструкции из прогнозируемого местоположения, и спустить конвейер и начать сначала, если это окажется неправильным, когда ветвь будет достигнута; в случае, когда подсказка верна, это сделает ветвь намного быстрее-точно, насколько быстрее будет зависеть от аппаратное обеспечение; и насколько это влияет на производительность кода, будет зависеть от того, какая доля времени подсказка верна.

например, на процессоре PowerPC unhinted ветвь может занять 16 циклов, правильно намекнул один 8 и неправильно намекнул один 24. В самых сокровенных петлях хороший намек может иметь огромное значение.

мобильность не проблема, по - видимому, определение в ТВ-платформу "заголовок", можно просто определить "скорее всего" и "вряд ли" ничего для платформ, которые не поддерживают статические подсказки ветвей.


long __builtin_expect(long EXP, long C);

эта конструкция сообщает компилятору, что выражение EXP скорее всего, будет иметь значение C. возвращаемое значение EXP. _ _ builtin _ expect предназначен для использования в качестве условного выражение. Почти во всех случаях он будет использоваться в контекст булевых выражений, в этом случае это много удобнее определить два вспомогательных макроса:

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

эти макросы можно использовать как в

if (likely(a > 1))

ссылка: https://www.akkadia.org/drepper/cpumemory.pdf


(общий комментарий - другие ответы охватывают детали)

нет причин, по которым вы должны потерять мобильность, используя их.

У вас всегда есть возможность создать простой нулевой эффект "inline" или макрос, который позволит вам компилировать на других платформах с другими компиляторами.

вы просто не получите выгоду от оптимизации, если вы на других платформах.


согласно комментарию Коди, это не имеет ничего общего с Linux, но это подсказка компилятору. Что произойдет, будет зависеть от архитектуры и версии компилятора.

эта особенность в Linux несколько неправильно используется в драйверах. As osgx в семантика горячего атрибута, либо hot или cold функция, вызываемая в блоке, может автоматически намекать, что условие вероятно или нет. Например, dump_stack() отмечается cold Так что это избыточно,

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

будущие версии gcc может выборочно встроить функцию на основе этих подсказок. Были также предложения, что это не boolean, но оценка, как в скорее всего, etc. Как правило, предпочтительнее использовать какой-то альтернативный механизм, например cold. Нет причин использовать его в любом месте, кроме горячих путей. То, что компилятор будет делать на одной архитектуре, может быть совершенно другим другой.


во многих версиях linux вы можете найти complier.h в / usr / linux/, вы можете включить его для использования просто. И другое мнение, маловероятно () более полезно, чем вероятно (), потому что

if ( likely( ... ) ) {
     doSomething();
}

Он также может быть оптимизирован во многих компиляторах.

и кстати, если вы хотите наблюдать за подробным поведением кода, Вы можете сделать просто следующее:

тест gcc-C.с objdump -d проверит.o > obj.s

затем откройте obj.с вами могу найти ответ.


Это подсказки компилятору для генерации префиксов подсказок в ветвях. На x86 / x64 они занимают один байт, поэтому вы получите максимум однобайтовое увеличение для каждой ветви. Что касается производительности, это полностью зависит от приложения-в большинстве случаев предсказатель ветвей на процессоре будет игнорировать их в эти дни.

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


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

Как строятся инструкции ветви, зависят от архитектуры процессора.