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

StackOverflow https://stackoverflow.com/questions/109710

Вопрос

Я копался в некоторых частях ядра 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)

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

Это было полезно?

Решение

Они являются подсказкой компилятору о необходимости выдавать инструкции, которые заставят предсказание ветвления отдать предпочтение «вероятной» стороне инструкции перехода.Это может быть большой победой: если прогноз верен, это означает, что инструкция перехода практически бесплатна и займет ноль циклов.С другой стороны, если прогноз неверен, это означает, что конвейер процессора необходимо очистить, а это может занять несколько циклов.Пока прогноз верен в большинстве случаев, это, как правило, положительно сказывается на производительности.

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

Они заставляют компилятор выдавать соответствующие подсказки ветвей там, где их поддерживает оборудование.Обычно это означает просто изменение нескольких битов в коде операции инструкции, поэтому размер кода не изменится.ЦП начнет извлекать инструкции из предсказанного места, очистит конвейер и начнет все сначала, если при достижении ветки окажется, что это неправильно;в случае, если подсказка верна, это сделает ветку намного быстрее — насколько быстрее будет зависеть от аппаратного обеспечения;и насколько это повлияет на производительность кода, будет зависеть от того, в какой части времени подсказка верна.

Например, на процессоре PowerPC ветвь без подсказки может занять 16 тактов, правильно подсказанная — 8, а неправильная — 24.Во внутренних циклах хорошие хинтинги могут иметь огромное значение.

Переносимость на самом деле не является проблемой - предположительно, определение находится в заголовке каждой платформы;вы можете просто ничего не определять «вероятно» и «маловероятно» для платформ, которые не поддерживают статические подсказки ветвей.

long __builtin_expect(long EXP, long C);

Эта конструкция сообщает компилятору, что Expression 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

(общий комментарий - другие ответы раскрывают детали)

Нет никаких причин, по которым вы должны терять мобильность при их использовании.

У вас всегда есть возможность создать простой «встроенный» макрос с нулевым эффектом, который позволит вам компилировать на других платформах с помощью других компиляторов.

Вы просто не получите преимуществ от оптимизации, если используете другие платформы.

Судя по комментарию Коди, это не имеет никакого отношения к Linux, а является подсказкой компилятору.Что произойдет, будет зависеть от архитектуры и версии компилятора.

Эта особенность Linux несколько неправильно используется в драйверах.Как osgx указывает в семантика горячего атрибута, любой hot или cold Функция, вызываемая в блоке, может автоматически указать, вероятно ли условие или нет.Например, dump_stack() отмечен cold так что это лишнее,

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

Будущие версии gcc может выборочно встроить функцию на основе этих подсказок.Были также предположения, что это не так. boolean, но оценка как в вероятно, и т. д.Как правило, предпочтительнее использовать какой-либо альтернативный механизм, например cold.Нет смысла использовать его где-либо, кроме горячих путей.То, что компилятор будет делать на одной архитектуре, может совершенно отличаться на другой.

Во многих выпусках Linux вы можете найти complier.h в /usr/linux/, вы можете просто включить его для использования.И еще мнение, вряд ли() полезнее вероятно(), потому что

if ( likely( ... ) ) {
     doSomething();
}

его также можно оптимизировать во многих компиляторах.

И кстати, если вы хотите понаблюдать за детальным поведением кода, вы можете сделать следующее:

gcc -c test.c objdump -d test.o> obj.s

Затем, открыв obj.s, вы сможете найти ответ.

Это подсказки компилятору для создания префиксов подсказок в ветвях.На x86/x64 они занимают один байт, поэтому вы получите увеличение максимум на один байт для каждой ветви.Что касается производительности, то она полностью зависит от приложения — в большинстве случаев в наши дни предсказатель ветвей на процессоре их игнорирует.

Редактировать:Забыл об одном месте, с которым они действительно могут помочь.Это может позволить компилятору изменить порядок графа потока управления, чтобы уменьшить количество ветвей, выбранных для «вероятного» пути.Это может заметно улучшить циклы, в которых вы проверяете несколько вариантов выхода.

Это функции GCC, позволяющие программисту подсказать компилятору, какое наиболее вероятное условие ветвления будет в данном выражении.Это позволяет компилятору создавать инструкции ветвления так, чтобы в наиболее распространенном случае для выполнения требовалось наименьшее количество инструкций.

Способ построения инструкций ветвления зависит от архитектуры процессора.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top