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