Как работают вероятные/маловероятные макросы в ядре 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)
Я знаю, что они предназначены для оптимизации, но как они работают? И сколько можно ожидать от снижения производительности/размера? И стоит ли хлопот (и, вероятно, потерять переносимость), по крайней мере, в узком коде (в пользовательском пространстве, конечно).
Ответы
Ответ 1
Они намекают на то, что компилятор выдаст инструкции, которые заставят предсказание ветки одобрить "вероятную" сторону инструкции перехода. Это может быть большой победой, если предсказание правильное, это означает, что инструкция перехода в основном свободна и будет принимать нулевые циклы. С другой стороны, если предсказание ошибочно, то это означает, что конвейер процессора должен быть сброшен, и он может стоить несколько циклов. До тех пор, пока прогноз будет правильным в большинстве случаев, это будет иметь тенденцию быть хорошим для производительности.
Как и во всех таких оптимизациях производительности, вы должны делать это только после обширного профилирования, чтобы убедиться, что код действительно находится в узком месте и, вероятно, с учетом микроуровня, что он запускается в замкнутом цикле. Вообще разработчики Linux довольно опытные, поэтому я бы предположил, что они это сделали бы. Они не слишком заботятся о переносимости, поскольку они нацелены только на gcc, и у них есть очень тесная идея сборки, которую они хотят создать.
Ответ 2
Это макросы, которые дают подсказки компилятору о том, каким образом может идти ветвь. Макросы расширяются до определенных расширений GCC, если они доступны.
GCC использует их для оптимизации прогнозирования ветвлений. Например, если у вас есть что-то вроде следующего
if (unlikely(x)) {
dosomething();
}
return x;
Тогда он может реструктурировать этот код так, чтобы он выглядел примерно так:
if (!x) {
return x;
}
dosomething();
return x;
Преимущество этого состоит в том, что когда процессор берет ветвь в первый раз, это приводит к значительным накладным расходам, потому что он мог спекулятивно загружать и выполнять код дальше. Когда он определит, что он возьмет ветвь, он должен аннулировать это и начать с цели ветвления.
Большинство современных процессоров теперь имеют своего рода предсказание ветвления, но это помогает только тогда, когда вы уже проходили ветвь, а ветвь все еще находится в кеше предсказания ветвления.
Существует ряд других стратегий, которые компилятор и процессор могут использовать в этих сценариях. Вы можете найти более подробную информацию о том, как предсказатели веток работают в Википедии: http://en.wikipedia.org/wiki/Branch_predictor
Ответ 3
Пусть декомпилирует, чтобы увидеть, что с ним делает 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 $0x8,%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 $0x1,%edx
15: be 00 00 00 00 mov $0x0,%esi
16: R_X86_64_32 .rodata.str1.1
1a: bf 01 00 00 00 mov $0x1,%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 $0x0,%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 $0x8,%rsp
34: c3 retq
Порядок команд в памяти не изменился: сначала верните printf
, а затем puts
и retq
.
С __builtin_expect
Теперь замените if (i)
на:
if (__builtin_expect(i, 0))
и получим:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%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 $0x0,%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 $0x8,%rsp
20: c3 retq
21: ba 01 00 00 00 mov $0x1,%edx
26: be 00 00 00 00 mov $0x0,%esi
27: R_X86_64_32 .rodata.str1.1
2b: bf 01 00 00 00 mov $0x1,%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
, чем без, Процессоры действительно умны в те дни. Мои наивные попытки здесь.
Ответ 4
Они заставляют компилятор испускать соответствующие подсказки ветки, где их поддерживает аппаратное обеспечение. Как правило, это просто означает, что несколько битов в коде операции, поэтому размер кода не изменится. ЦП начнет извлечение инструкций из прогнозируемого местоположения и очистку конвейера и начнется, если это окажется неправильным при достижении ветки; в случае, когда подсказка верна, это сделает ветвь намного быстрее - точно, насколько быстрее будет зависеть аппаратное обеспечение; и насколько это влияет на производительность кода, будет зависеть от того, какая пропорция подсказки времени верна.
Например, на процессоре PowerPC целая ветка может занять 16 циклов, правильно намеченный символ 8 и неверно намеченный номер 24. В самых внутренних петлях хороший намек может иметь огромное значение.
Переносимость на самом деле не проблема - предположительно, определение относится к заголовку для каждой платформы; вы можете просто определить "вероятный" и "маловероятный" ни для чего для платформ, которые не поддерживают подсказки статической ветки.
Ответ 5
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
Ответ 6
(общий комментарий - другие ответы охватывают детали)
Нет причин, по которым вы должны потерять переносимость, используя их.
У вас всегда есть возможность создать простой nil-эффект "inline" или макрос, который позволит вам скомпилировать на других платформах с другими компиляторами.
Вы просто не сможете воспользоваться оптимизацией, если находитесь на других платформах.
Ответ 7
Во многих версиях Linux вы можете найти complier.h в /usr/linux/, вы можете просто включить его для использования. И другое мнение, маловероятное() более полезно, а не скорее(), потому что
if ( likely( ... ) ) {
doSomething();
}
он также может быть оптимизирован во многих компиляторах.
И, кстати, если вы хотите наблюдать подробное поведение кода, вы можете сделать следующее:
gcc -c test.c objdump -d test.o > obj.s
Затем, откройте obj.s, вы можете найти ответ.
Ответ 8
В соответствии с комментарием Cody это не имеет ничего общего с Linux, но это намек на компилятор. То, что произойдет, будет зависеть от архитектуры и версии компилятора.
Эта особенность в Linux несколько неверно используется в драйверах. Поскольку osgx указывает в семантике горячего атрибута, любая функция hot
или cold
, вызываемая в блоке может автоматически намекнуть, что это условие вероятно или нет. Например, dump_stack()
отмечен cold
, поэтому это избыточно,
if(unlikely(err)) {
printk("Driver error found. %d\n", err);
dump_stack();
}
Будущие версии gcc
могут выборочно включать функцию, основанную на этих подсказках. Также высказывались предположения, что это не boolean
, а оценка, как, скорее всего, и т.д. Вообще, предпочтительно использовать некоторый альтернативный механизм, например cold
. Нет причин использовать его в любом месте, кроме горячих путей. То, что компилятор будет делать на одной архитектуре, может быть совершенно другим.
Ответ 9
Они намекают на компилятор, чтобы генерировать префикс подсказки для ветвей. На x86/x64 они занимают один байт, поэтому вы получите не более одного байта для каждой ветки. Что касается производительности, то это полностью зависит от приложения - в большинстве случаев предсказатель ветвления на процессоре будет игнорировать их в эти дни.
Изменить: Забыл о одном месте, с которым они действительно могут помочь. Он может позволить компилятору переупорядочить график потока управления, чтобы уменьшить количество ветвей, принятых для "вероятного" пути. Это может иметь заметное улучшение в циклах, где вы проверяете несколько случаев выхода.
Ответ 10
Это функции GCC для программиста, чтобы дать подсказку компилятору о том, какое наиболее вероятное условие ветвления будет в данном выражении. Это позволяет компилятору строить инструкции ветвления, так что наиболее распространенный случай занимает наименьшее количество команд для выполнения.
Как создаются инструкции ветвления, зависит от архитектуры процессора.