在 if-else if 链中使用 Likely() / Unlikely() 预处理器宏
Using Likely() / Unlikely() Preprocessor Macros in if-else if chain
如果我有:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
if (A)
return true;
else if (B)
return false;
...
else if (Z)
return true;
else
//this will never really happen!!!!
raiseError();
return false;
我可以像 else if (likely(Z))
一样在最后一个条件检查周围放置 likely() 来表示如果编译器不影响先前检查的分支预测,最后的语句(else)是不太可能的吗?
基本上,如果存在带有分支预测器提示的单个条件语句,GCC 是否会尝试优化整个 if-else if 块?
你应该明确说明:
if (A)
return true;
else if (B)
return true;
...
else if (Y)
return true;
else {
if (likely(Z))
return true;
raiseError();
return false;
}
现在编译器清楚地理解你的意图并且不会重新分配其他分支概率。代码的可读性也增加了。
P.S。我建议你重写 also likely 和 unlikely 的方式 Linux kernel do to protect from silent integral casts:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
一般来说,GCC 假定 if 语句中的条件为真 - 也有例外,但它们是上下文相关的。
extern int s(int);
int f(int i) {
if (i == 0)
return 1;
return s(i);
}
产生
f(int):
testl %edi, %edi
jne .L4
movl , %eax
ret
.L4:
jmp s(int)
而
extern int t(int*);
int g(int* ip) {
if (!ip)
return 0;
return t(ip);
}
产生:
g(int*):
testq %rdi, %rdi
je .L6
jmp t(int*)
.L6:
xorl %eax, %eax
ret
(参见 godbolt)
注意 f
中的分支是 jne
(假设条件为真),而在 g
中假设条件为假。
现在与以下比较:
extern int s(int);
extern int t(int*);
int x(int i, int* ip) {
if (!ip)
return 1;
if (!i)
return 2;
if (s(i))
return 3;
if (t(ip))
return 4;
return s(t(ip));
}
产生
x(int, int*):
testq %rsi, %rsi
je .L3 # first branch: assumed unlikely
movl , %eax
testl %edi, %edi
jne .L12 # second branch: assumed likely
ret
.L12:
pushq %rbx
movq %rsi, %rbx
call s(int)
movl %eax, %edx
movl , %eax
testl %edx, %edx
je .L13 # third branch: assumed likely
.L2:
popq %rbx
ret
.L3:
movl , %eax
ret
.L13:
movq %rbx, %rdi
call t(int*)
movl %eax, %edx
movl , %eax
testl %edx, %edx
jne .L2 # fourth branch: assumed unlikely!
movq %rbx, %rdi
call t(int*)
popq %rbx
movl %eax, %edi
jmp s(int)
在这里我们看到了一个上下文因素:GCC 发现它可以在这里重复使用 L2
,因此它决定认为最终条件不太可能,以便它可以发出更少的代码。
让我们看看你给出的例子的汇编:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
extern void raiseError();
int f(int A, int B, int Z)
{
if (A)
return 1;
else if (B)
return 2;
else if (Z)
return 3;
raiseError();
return -1;
}
程序集looks like this:
f(int, int, int):
movl , %eax
testl %edi, %edi
jne .L9
movl , %eax
testl %esi, %esi
je .L11
.L9:
ret
.L11:
testl %edx, %edx
je .L12 # branch if !Z
movl , %eax
ret
.L12:
subq , %rsp
call raiseError()
movl $-1, %eax
addq , %rsp
ret
请注意,当 !Z 为真时生成的代码分支,它已经表现得好像 Z 是可能的。如果我们告诉它 Z 很可能会发生什么?
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
extern void raiseError();
int f(int A, int B, int Z)
{
if (A)
return 1;
else if (B)
return 2;
else if (likely(Z))
return 3;
raiseError();
return -1;
}
现在we get
f(int, int, int):
movl , %eax
testl %edi, %edi
jne .L9
movl , %eax
testl %esi, %esi
je .L11
.L9:
ret
.L11:
movl , %eax # assume Z
testl %edx, %edx
jne .L9 # but branch if Z
subq , %rsp
call raiseError()
movl $-1, %eax
addq , %rsp
ret
这里的要点是,您在使用这些宏时应谨慎,并仔细检查前后的代码以确保您获得预期的结果,并进行基准测试(例如使用 perf)以确保处理器正在做出与您生成的代码一致的预测。
如果我有:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
if (A)
return true;
else if (B)
return false;
...
else if (Z)
return true;
else
//this will never really happen!!!!
raiseError();
return false;
我可以像 else if (likely(Z))
一样在最后一个条件检查周围放置 likely() 来表示如果编译器不影响先前检查的分支预测,最后的语句(else)是不太可能的吗?
基本上,如果存在带有分支预测器提示的单个条件语句,GCC 是否会尝试优化整个 if-else if 块?
你应该明确说明:
if (A)
return true;
else if (B)
return true;
...
else if (Y)
return true;
else {
if (likely(Z))
return true;
raiseError();
return false;
}
现在编译器清楚地理解你的意图并且不会重新分配其他分支概率。代码的可读性也增加了。
P.S。我建议你重写 also likely 和 unlikely 的方式 Linux kernel do to protect from silent integral casts:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
一般来说,GCC 假定 if 语句中的条件为真 - 也有例外,但它们是上下文相关的。
extern int s(int);
int f(int i) {
if (i == 0)
return 1;
return s(i);
}
产生
f(int):
testl %edi, %edi
jne .L4
movl , %eax
ret
.L4:
jmp s(int)
而
extern int t(int*);
int g(int* ip) {
if (!ip)
return 0;
return t(ip);
}
产生:
g(int*):
testq %rdi, %rdi
je .L6
jmp t(int*)
.L6:
xorl %eax, %eax
ret
(参见 godbolt)
注意 f
中的分支是 jne
(假设条件为真),而在 g
中假设条件为假。
现在与以下比较:
extern int s(int);
extern int t(int*);
int x(int i, int* ip) {
if (!ip)
return 1;
if (!i)
return 2;
if (s(i))
return 3;
if (t(ip))
return 4;
return s(t(ip));
}
产生
x(int, int*):
testq %rsi, %rsi
je .L3 # first branch: assumed unlikely
movl , %eax
testl %edi, %edi
jne .L12 # second branch: assumed likely
ret
.L12:
pushq %rbx
movq %rsi, %rbx
call s(int)
movl %eax, %edx
movl , %eax
testl %edx, %edx
je .L13 # third branch: assumed likely
.L2:
popq %rbx
ret
.L3:
movl , %eax
ret
.L13:
movq %rbx, %rdi
call t(int*)
movl %eax, %edx
movl , %eax
testl %edx, %edx
jne .L2 # fourth branch: assumed unlikely!
movq %rbx, %rdi
call t(int*)
popq %rbx
movl %eax, %edi
jmp s(int)
在这里我们看到了一个上下文因素:GCC 发现它可以在这里重复使用 L2
,因此它决定认为最终条件不太可能,以便它可以发出更少的代码。
让我们看看你给出的例子的汇编:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
extern void raiseError();
int f(int A, int B, int Z)
{
if (A)
return 1;
else if (B)
return 2;
else if (Z)
return 3;
raiseError();
return -1;
}
程序集looks like this:
f(int, int, int):
movl , %eax
testl %edi, %edi
jne .L9
movl , %eax
testl %esi, %esi
je .L11
.L9:
ret
.L11:
testl %edx, %edx
je .L12 # branch if !Z
movl , %eax
ret
.L12:
subq , %rsp
call raiseError()
movl $-1, %eax
addq , %rsp
ret
请注意,当 !Z 为真时生成的代码分支,它已经表现得好像 Z 是可能的。如果我们告诉它 Z 很可能会发生什么?
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
extern void raiseError();
int f(int A, int B, int Z)
{
if (A)
return 1;
else if (B)
return 2;
else if (likely(Z))
return 3;
raiseError();
return -1;
}
现在we get
f(int, int, int):
movl , %eax
testl %edi, %edi
jne .L9
movl , %eax
testl %esi, %esi
je .L11
.L9:
ret
.L11:
movl , %eax # assume Z
testl %edx, %edx
jne .L9 # but branch if Z
subq , %rsp
call raiseError()
movl $-1, %eax
addq , %rsp
ret
这里的要点是,您在使用这些宏时应谨慎,并仔细检查前后的代码以确保您获得预期的结果,并进行基准测试(例如使用 perf)以确保处理器正在做出与您生成的代码一致的预测。