Цель __builtin_unreachable
- помочь компилятору удалить мертвый код (который, как знает программист, никогда не будет выполнен) и линеаризовать код, сообщив компилятору, что путь «холодный».Рассмотрим следующее:
void exit_if_true(bool x);
int foo1(bool x)
{
if (x) {
exit_if_true(true);
//__builtin_unreachable(); // we do not enable it here
} else {
std::puts("reachable");
}
return 0;
}
int foo2(bool x)
{
if (x) {
exit_if_true(true);
__builtin_unreachable(); // now compiler knows exit_if_true
// will not return as we are passing true to it
} else {
std::puts("reachable");
}
return 0;
}
Сгенерированный код:
foo1(bool):
sub rsp, 8
test dil, dil
je .L2 ; that jump is going to change
mov edi, 1
call exit_if_true(bool)
xor eax, eax ; that tail is going to be removed
add rsp, 8
ret
.L2:
mov edi, OFFSET FLAT:.LC0
call puts
xor eax, eax
add rsp, 8
ret
foo2(bool):
sub rsp, 8
test dil, dil
jne .L9 ; changed jump
mov edi, OFFSET FLAT:.LC0
call puts
xor eax, eax
add rsp, 8
ret
.L9:
mov edi, 1
call exit_if_true(bool)
Обратите внимание на различия:
xor eax, eax
и ret
были удалены, как теперь компиляторзнает, что это мертвый код. - Компилятор поменял местами порядок ветвлений: сначала идет ветвь с вызовом
puts
, так что условный переход может быть быстрее (прямые ветвления, которые не были приняты, быстрее как при предсказании, так и при прогнозировании).когда нет информации о предсказании).
Здесь предполагается, что ветвь, которая заканчивается вызовом функции noreturn
или __builtin_unreachable
, будет выполнена только один раз или приведет к вызову или исключению longjmp
бросить оба из них редко, и не нужно расставлять приоритеты во время оптимизации.
Вы пытаетесь использовать его для другой цели - предоставляя компилятору информацию о псевдонимах (и вы можете попробовать сделать то же самое для выравнивания),К сожалению, GCC не поддерживает такие проверки адресов.
Как вы заметили, добавление __restrict__
помогает.Так что __restrict__
работает для псевдонимов, __builtin_unreachable
- нет.
Посмотрите на следующий пример, в котором используется __builtin_assume_aligned
:
void copy1(int *__restrict__ dst, const int *__restrict__ src)
{
if (reinterpret_cast<uintptr_t>(dst) % 16 == 0) __builtin_unreachable();
if (reinterpret_cast<uintptr_t>(src) % 16 == 0) __builtin_unreachable();
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
}
void copy2(int *__restrict__ dst, const int *__restrict__ src)
{
dst = static_cast<int *>(__builtin_assume_aligned(dst, 16));
src = static_cast<const int *>(__builtin_assume_aligned(src, 16));
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
dst[3] = src[3];
}
Сгенерированный код:
copy1(int*, int const*):
movdqu xmm0, XMMWORD PTR [rsi]
movups XMMWORD PTR [rdi], xmm0
ret
copy2(int*, int const*):
movdqa xmm0, XMMWORD PTR [rsi]
movaps XMMWORD PTR [rdi], xmm0
ret
Можно предположить, что компилятор может понять, что dst % 16 == 0
означает, что указатель выровнен на 16 байтов, но это не так.Таким образом, используются невыровненные хранилища и нагрузки, а вторая версия генерирует более быстрые инструкции, требующие выравнивания адреса.