Как работают вероятные/маловероятные макросы в ядре Linux и в чем их выгода? - программирование
Подтвердить что ты не робот

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

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

4b9b3361

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

Как создаются инструкции ветвления, зависит от архитектуры процессора.