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