Помощь компилятору в оптимизации условий
Взглянув на код ядра Linux, вы увидите множество условий, заключенных в вероятные и маловероятные макросы. Эти макросы вызывают директивы компилятора, которые дают ему подсказку относительно участка кода, который следует оптимизировать для повышения производительности.
Например, следующая функция вставки временного буфера трансляции (TLB) из кода ядра Linux содержит условие if с директивой likely
. Здесь автор сообщает компилятору, что очень вероятно, что поиск записи TLB не удастся.
static void tlb_entry_insert(unsigned int pd0, pte_t pd1) { unsigned int idx; idx = tlb_entry_lkup(pd0); if (likely(idx & TLB_LKUP_ERR)) write_aux_reg(ARC_REG_TLBCOMMAND, TLBGetIndex); write_aux_reg(ARC_REG_TLBPD1, pd1); write_aux_reg(ARC_REG_TLBCOMMAND, TLBWrite); }
Точно так же разработчики могут использовать макрос unlikely
, чтобы сигнализировать компилятору об оптимизации кода для взятой ветви else. См. Следующий пример кода ядра Linux о том, как используется макрос unlikely
. Здесь компилятор получает указание, что одновременная очистка 32 или более страниц маловероятна. Компилятор должен оптимизировать менее 32 страниц.
void local_flush_tlb_kernel_range(unsigned long start, unsigned long end) { unsigned long flags; if (unlikely((end - start) >= PAGE_SIZE * 32)) { local_flush_tlb_all(); return; } start &= PAGE_MASK; local_irq_save(flags); while (start < end) { tlb_entry_erase(start); start += PAGE_SIZE; } utlb_invalidate(); local_irq_restore(flags); }
Как компилятор оптимизирует вероятные и маловероятные директивы?
Изучите генерацию кода на предмет if и else
Давайте воспользуемся простым примером, чтобы понять оптимизацию. Следующий код содержит единственный if-else. Соответствующая сборка показана справа. Щелкните изображение, чтобы открыть код в Обозревателе компилятора.
Давайте проследим путь в ассемблерном коде для ветвей if и else:
Выполнение кода, когда argc ›0 (берется if-leg)
sub rsp,8
- выделить место в стеке.test edi, edi
- проверьте значениеargc
.jle .L2
- Перейти, если меньше, чем равно. Этот прыжок не выполняется.mov edi, OFFSET FLAT:.LC0
- скопируйте указатель на строку"Positive\n"
в регистрedi
.call puts
- Вызов функции put.xor eax, eax
- Установитеeax
на 0 (компиляторы используют саму XOR, так как это быстрее, чем инструкция по установкеeax
на 0).add rsp, 8
- Освободить место в стеке.ret
- возврат сmain
.
Выполнение кода, когда argc ≤ 0 (берется else-leg)
sub rsp,8
- выделить место в стеке.test edi, edi
- проверьте значениеargc
.jle .L2
- Перейти, если меньше, чем равно. Этот прыжок сделан..L2
- Переход к метке.L2
.mov edi, OFFSET FLAT:.LC1
- скопируйте указатель на строку"Zero or Negative\n"
в регистрedi
.call puts
- Вызов функции put..L3
- Безусловный переход к.L3
.xor eax, eax
- установитеeax
на 0.add rsp, 8
- Освободить место в стеке.ret
- возврат сmain
.
Из трассировки кода ясно, что компилятор оптимизировал ветвь if для выполнения кода без каких-либо ветвлений. Нога else встречает две дополнительные ветви (шаги 4 и 7 в трассе ветви else - argc ≤ 0).
Оптимизируйте ногу else, используя маловероятную директиву
Что, если приложение требует оптимизации ветви else? Это достигается за счет добавления макроса unlikely
в условие if. Щелкните изображение, чтобы открыть код в Обозревателе компилятора.
Теперь мы видим, что компилятор изменил оптимизацию. Теперь нога else выполняется без выполнения ответвления. Нога if встречает две ветви.
Выполнение кода, когда argc ≤0 (берется else-leg)
sub rsp,8
- выделить место в стеке.test edi, edi
- проверьте значениеargc
.jg .L6
- Перейти, если больше. Этот прыжок не выполняется.mov edi, OFFSET FLAT:.LC1
- скопируйте указатель на строку"Zero or Negative\n"
в регистрedi
.call puts
- Вызов функции put.xor eax, eax
- установитеeax
на 0.add rsp, 8
- Освободить место в стеке.ret
- возврат сmain
.
Выполнение кода, когда argc ›0 (берется if-leg)
sub rsp,8
- выделить место в стеке.test edi, edi
- проверьте значениеargc
.jg .L6
- Перейти, если больше. Этот прыжок сделан..L6
- Выполнен переход к метке.L6
.mov edi, OFFSET FLAT:.LC0
- скопируйте указатель на строку"Positive\n"
в регистрedi
.call puts
- Вызов функции put..L3
- Безусловный переход к.L3
.xor eax, eax
- установитеeax
на 0.add rsp, 8
- Освободить место в стеке.ret
- возврат сmain
.
Использование вероятной директивы оптимизирует if-часть кода
В этом примере использование макроса likely
генерирует тот же код, что и отсутствие указания какой-либо директивы. If-leg оптимизирован для выполнения без каких-либо ветвлений.
Определение вероятных и маловероятных макросов
В настоящее время необходимо определить макросы likely
и unlikely
для вызова функции __builtin_expect
.
#define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
вероятные и маловероятные атрибуты в C ++ 20
Если использование макросов likely
и unlikely
казалось немного случайным, обратите внимание на атрибуты C ++ 20 [[likely]]
и [[unlikely]]
. Эти атрибуты также будут поддерживаться в switch-statement.
int foo(int i) { switch(i) { case 1: handle1(); break; [[likely]] case 2: handle2(); break; } }
Предложение C ++ 20 для [[likely]]
и [[unlikely]]
описывает преимущества предоставления прямых подсказок компилятору.
Влияние вероятных и маловероятных подсказок на производительность
Каков выигрыш в производительности от использования этих макросов? Давайте воспользуемся примером из предложения C ++ 20, чтобы понять разницу в производительности, вызванную обновлением функции clamp
в этом примере.
Давайте рассмотрим код функции с подсказкой unlikely
и без нее.
Без намека
Здесь компилятор генерирует довольно жесткий код с очень небольшим пространством для оптимизации.
xor eax, eax
- Возвращаемое значение по умолчанию - 0test edi, edi
- проверьте, равно лиi
0cmp edi, 65535
- Сравнитеi
с 65535mov eax, -1
- По умолчанию возвращается значение 65535cmovle eax, edi
- скопируйтеi
в возвращаемое значение, еслиi
≤ 65535ret
- возврат
С подсказкой (выход за пределы диапазона маловероятен)
Использование макроса unlikely
позволяет компилятору выполнить оптимизацию, удалив инструкцию из вероятного пути.
test edi, edi
- проверьте, равно лиi
0cmp edi, 65535
- сравнитеi
с 65535mov eax, -1
- по умолчанию возвращается значение 65535cmovle eax, edi
- скопируйтеi
в возвращаемое значение, еслиi
≤ 65535ret
- возврат
Из приведенного выше сравнения мы видим, что компилятор удалил по инструкции вероятного пути - xor eax, eax
.
Результаты тестов
Из результатов теста на бумаге мы видим, что версия функции clamp
с unlikely
приводит к довольно значительному сокращению времени выполнения, если из значения диапазона от 0,1% до 10%. После этого. Как и ожидалось, придется заплатить небольшой штраф, если данные содержат значения, которые принимают маловероятный отрезок.
Когда мне следует давать компилятору явные подсказки?
В большей части кода не следует жертвовать удобочитаемостью, засоряя код макросами likely
и unlikely
.
- В большинстве сценариев кэш прогнозирования ветвлений процессора довольно хорошо справляется с предсказанием вероятного перехода. Особенно это касается внутренних петель.
- Тем не менее, наиболее важные для производительности внутренние циклы в коде могут увидеть улучшения в подсказках производительности.
- Для записи макросов может быть полезен макрос
unlikely
. Это сообщает компилятору, что включение ведения журнала маловероятно. Это поможет снизить стоимость проверки уровня LOG, если операторы, которые незримо включены в большую часть вашего кода.
#define LOG(level, message) if (unlikely(enabledLogLevel > level))\ sendLog(message);