[英]What optimizations does __builtin_unreachable facilitate?
从 gcc 的文档来看
如果控制流到达
__builtin_unreachable
的点,则程序未定义。
我认为__builtin_unreachable
可以以各种创造性的方式用作优化器的提示。 所以我做了一个小实验
void stdswap(int& x, int& y)
{
std::swap(x, y);
}
void brswap(int& x, int& y)
{
if(&x == &y)
__builtin_unreachable();
x ^= y;
y ^= x;
x ^= y;
}
void rswap(int& __restrict x, int& __restrict y)
{
x ^= y;
y ^= x;
x ^= y;
}
编译为(g++ -O2)
stdswap(int&, int&):
mov eax, DWORD PTR [rdi]
mov edx, DWORD PTR [rsi]
mov DWORD PTR [rdi], edx
mov DWORD PTR [rsi], eax
ret
brswap(int&, int&):
mov eax, DWORD PTR [rdi]
xor eax, DWORD PTR [rsi]
mov DWORD PTR [rdi], eax
xor eax, DWORD PTR [rsi]
mov DWORD PTR [rsi], eax
xor DWORD PTR [rdi], eax
ret
rswap(int&, int&):
mov eax, DWORD PTR [rsi]
mov edx, DWORD PTR [rdi]
mov DWORD PTR [rdi], eax
mov DWORD PTR [rsi], edx
ret
我假设从优化器的角度来看, stdswap
和rswap
是最佳的。 为什么brswap
不被编译成同样的东西? 我可以用__builtin_unreachable
让它编译成同样的东西吗?
__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字节对齐的,但事实并非如此。 因此使用未对齐的存储和加载,而第二个版本生成更快的指令,需要对齐地址。
我认为你是试图微量优化你的代码错误的方向错误。
__builtin_unreachable以及__builtin_expect执行预期的操作 - 在您的情况下jnz
使用的if运算符中删除不必要的cmp
和jnz
。
编译器应该使用您编写的C代码生成机器代码,以生成可预测的程序。 在优化过程中,它能够找到并优化(即用更好的机器代码版本替换)一些模式,当优化算法已知时 - 这样的优化不会破坏程序行为。
比如像
char a[100];
for(int i=0; i < 100; i++)
a[i] = 0;
将替换单个调用库std :: memset(a,0,100),它是使用汇编实现的,并且是当前CPU架构的最佳选择。
以及编译器能够检测
x ^= y;
y ^= x;
x ^= y;
并用最简单的mashie代码替换它。
我认为你的if运算符和未达到的指令会影响编译器优化器,因此无法进行优化。
在交换两个整数的情况下,第三个临时交换变量可以通过编译器自己删除,即它会像
movl $2, %ebx
movl $1, %eax
xchg %eax,%ebx
其中ebx和eax寄存器值实际上是你的x和y。 你可以像自己一样实现它
void swap_x86(int& x, int& y)
{
__asm__ __volatile__( "xchg %%rax, %%rbx": "=a"(x), "=b"(y) : "a"(x), "b"(y) : );
}
...
int a = 1;
int b = 2;
swap_x86(a,b);
什么时候使用__builtin_unreachable? 可能当你知道某些情况几乎不可能时,但逻辑上可能会发生。 即你有一些功能
void foo(int v) {
switch( v ) {
case 0:
break;
case 1:
break;
case 2:
break;
case 3:
break;
default:
__builtin_unreachable();
}
}
并且您知道v
参数值始终在0和3之间。但是,int范围是-2147483648
到2147483647
(当int是32位类型时),编译器不知道实际值范围并且无法删除默认块(如以及一些cmp指令等),但如果你不将这个块添加到交换机中,它会警告你。 所以在这种情况下__builtin_unreachable
可能有所帮助。
可能当您知道某些情况实际上是不可能的,但从逻辑上讲它可能会发生。
使用 __builtin_unreachable() 时要非常小心; 默认情况下:案例标签。 我花了几天时间试图找出 .rodata 区域的随机分支。 在我的例子中,有 0 到 25 个案例标签和一个默认值:案例,用于从 0 到 128 的枚举。以及导致问题的开关值 71。 6.3.0 有范围检查代码,但 8.3.0 对其进行了优化。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.