Я смотрел код в некоторых частях ядра Linux и нашел вызовы, подобные этому:
if (unlikely(fd < 0)) {
/* некоторый код */
}
Или:
if (likely(!err)) {
/* некоторый код */
}
Я нашел их определение:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
Я знаю, что они предназначены для оптимизации, но как они работают? И какого снижения производительности/размера можно ожидать от их использования? И стоит ли это хлопот (и потери переносимости, вероятно), по крайней мере в узком месте кода (на уровне пользователя, конечно).
Ответ 1
Эти макросы подсказывают компилятору, что нужно выдать инструкции, которые заставят предсказание ветвления предпочесть "вероятную" сторону инструкции перехода. Это может быть большим выигрышем, если предсказание верно, то это означает, что инструкция перехода практически бесплатна и займет ноль тактов. С другой стороны, если предсказание неверно, то это означает, что конвейер процессора должен быть очищен, а это может стоить несколько тактов. До тех пор, пока предсказание будет правильным большую часть времени, это будет благоприятно сказываться на производительности.
Как и все подобные оптимизации производительности, вы должны делать это только после тщательного профилирования, чтобы убедиться, что код действительно находится в узком месте, и, вероятно, учитывая микро архитектуру процессора, на котором код выполняется в «узком» месте цикла. Как правило, разработчики Linux довольно опытны, поэтому я полагаю, что они это сделают. Они не слишком заботятся о переносимости, поскольку используют только gcc, и у них есть конкретное представление о сборке, которую они хотят генерировать.
Ответ 2
Давайте проведем декомпиляцию, чтобы посмотреть, что с этим делает GCC 4.8.
Без __builtin_expect
#include "stdio.h"
#include "time.h"
int main() {
/* Используйте время, чтобы предотвратить его оптимизацию. */
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 и return, чтобы улучшить предсказание ветвлений, как упоминалось в других ответах. Таким образом, по сути это то же самое, что и:
int main() {
int i = !time(NULL);
if (i)
goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;
}
Ответ 3
Это макросы, которые дают компилятору подсказки о том, в какую сторону может пойти ветвление. Макросы расширяются до специфических для GCC расширений, если они доступны. GCC использует их для оптимизации предсказания ветвлений. Например, если у вас есть что-то вроде следующего:
if (unlikely(x)) {
dosomething();
}
return x;
Затем gcc может перестроить этот код так, чтобы он стал чем-то более похожим на этот:
if (!x) {
return x;
}
dosomething();
return x;
Преимущество этого заключается в том, что когда процессор выполняет ответвление в первый раз, возникают значительные накладные расходы, поскольку он мог спекулятивно загружать и выполнять код дальше. Когда процессор определяет, что он сделает ответвление, он должен аннулировать это и начать с цели ответвления.
Большинство современных процессоров сейчас имеют своего рода предсказание ветвлений, но это помогает только в том случае, если вы уже проходили через ветвление, и оно все еще находится в кэше предсказания ветвлений.
Существует ряд других стратегий, которые компилятор и процессор могут использовать в этих сценариях.
Ответ 4
Они заставляют компилятор выдавать соответствующие подсказки ветвления, если аппаратура их поддерживает. Обычно это означает лишь изменение нескольких битов в опкоде инструкции, так что размер кода не изменится. Процессор начнет выборку инструкций из предсказанного места, очистит конвейер и начнет выполнение сначала, если это окажется неверным, когда будет достигнуто ответвление; в случае, когда подсказка верна, это сделает ответвление намного быстрее — насколько именно быстрее, зависит от аппаратного обеспечения; и насколько это влияет на производительность кода.
Например, на процессоре PowerPC ветвление без подсказки может занять 16 тактов, правильно подсказанное — 8, а неправильно предсказанное — 24. Во внутренних циклах хорошая подсказка может иметь огромное значение. Переносимость не является проблемой — предположительно, определение находится в заголовке кода для каждой платформы; вы можете просто определить "вероятно" и "маловероятно" в «ничто» для платформ, которые не поддерживают статические подсказки ветвления.
Linux