Как работают вероятные/маловероятные макросы в ядре 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)


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

640   10  

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>

The 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 беспрепятственная ветвь может занять 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

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

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

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

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

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

эта особенность в Linux несколько неправильно используется в драйверах. Как 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 для программиста, чтобы дать подсказку компилятору о том, какое наиболее вероятное условие ветвления будет в данном выражении. Это позволяет компилятору построить инструкции ветви таким образом, что наиболее распространенный случай занимает наименьшее количество инструкций для выполнения.

Способ построения инструкций ветви зависит от архитектуры процессора.

Comments

    Ничего не найдено.