我有一个测试,要求如下:
if(variable==SOME_CONSTANT || variable==OTHER_CONSTANT)
在这种情况下,如果在一个分支超过第二个测试的平台上进行分支需要更多的周期,那么优化器是否允许将
||
视为简单的|
?我有一个测试,要求如下:
if(variable==SOME_CONSTANT || variable==OTHER_CONSTANT)
||
视为简单的|
?int? z = x + y;
其中x和y也是可空整数;这将生成为
int? z;
int? temp1 = x;
int? temp2 = y;
z = temp1.HasValue & temp2.HasValue ?
new int?(temp1.GetValueOrDefault() + temp2.GetValueOrDefault()) :
new int?();
&
而不是&&
。我知道调用HasValue
非常快,因此没有必要使用额外的分支逻辑来避免它。x>min&&x<max
(特别是当min
和max
是编译时常量时)转换为单个检查。这可以用2条指令完成,而不是分别在每个条件上进行分支。从范围的低端减去将会使输入小于0,所以减法加上无符号比较就可以进行范围检查。int foo(int x, int a, int b) {
if (10 < x && x < 100) {
return a;
}
return b;
}
使用gcc7.3编译(-O3
)生成x86-64 Windows ABI。你可以在Godbolt编译器资源管理器上查看ICC、clang或MSVC的输出,或者在ARM、MIPS等平台上查看gcc的输出:在Godbolt编译器资源管理器上查看。
foo(int, int, int):
sub ecx, 11 # x-11
mov eax, edx # retval = a;
cmp ecx, 89
cmovnb eax, r8d # retval = (x-11U) < 89U ? retval : b;
ret
cmov
(条件移动)。@HansPassant表示,.NET的编译器只倾向于对赋值操作进行此操作,因此,如果您将其写成C#源代码,则可能仅获得该asm:retval = (10 < x && x < 100) ? a : b;
。
sub
,然后是无符号比较/分支,而不是比较/cmov。int ext(void);
int bar(int x) {
if (10 < x && x < 100) {
return ext();
}
return 0;
}
# gcc -O3
sub ecx, 11
cmp ecx, 88
jbe .L7 # jump if ((unsigned)x-11U) <= 88U
xor eax, eax # return 0;
ret
.L7:
jmp ext() # tailcall ext()
我不确定现有的C#实现是否以相同的方式进行了这种优化,但对于所有可能的输入来说,这是易于实现的且有效的,所以他们应该这样做。
Godbolt没有C#编译器;如果有一个可以方便地显示汇编代码的在线C#编译器,那么在那里尝试这些函数会很有趣。(我认为它们是有效的C#语法,也是有效的C和有效的C ++)。
某些情况除了范围检查之外,将多个条件优化为单个分支或cmov可能会更加有利。x86不能非常高效地将比较结果存储到寄存器中(xor
-zero / cmp
/ setcc
),但在某些情况下,您只需要0 / 非零而不是0 / 1布尔值以稍后合并。x86的OR
指令设置标志位,因此您可以使用or
/jnz
跳转,以在任一寄存器为非零时跳转。(但请注意,在jcc
之前保存test reg,reg
只节省了代码大小;测试/跳转时,宏融合可行,但在or/jcc中不可行,因此or/test/jcc与or/jcc的uops数量相同。使用cmovcc或setcc可以节省一个uop。)
如果分支预测完美,则两个cmp
/jcc
可能仍然是最便宜的(由于宏融合:cmp
/jne
在最近的CPU上是单个uop),但如果没有,则两个条件一起可能会有更好的预测效果,或者使用CMOV会更好。
int foo(int x, int a, int b) {
if ((a-10) || (x!=5)) {
return a;
}
return b;
}
如果想让其他编译器生成像这样的代码,可能需要一些辅助。clang也需要类似的帮助来意识到它可以使用lea
进行复制和减法,而不需要在xor
之前使用mov
来避免破坏后续需要的输入。
int should_optimize_to(int x, int a, int b) {
// x!=10 fools compilers into missing the optimization
if ((a-10) | (x-5)) {
return a;
}
return b;
}
gcc、clang、msvc和ICC编译这个基本上是一样的:
# gcc7.3 -O3
lea eax, [rsi-10] # eax = a-10
sub edi, 5 # x-=5
or eax, edi # set flags
mov eax, edx
cmovne eax, esi
ret
这比clang的代码更加智能:在cmov
之前将mov
放入eax中可以创建指令级并行性。如果mov
具有非零延迟,则该延迟可以与创建cmov
的标志输入的延迟并行发生。
如果您想要这种优化,通常需要手动引导编译器实现它。
x-10 || a==5
(和可能存在的其他多个相等比较情况),这是合法且有利可图的,但gcc/clang/MSVC/ICC错过了这种优化(https://godbolt.org/g/74qpyn)。`OR`设置标志(但无法宏融合,因此在`jcc`之前省略`test`只能节省代码大小)。 - Peter Cordescmp
操作,每个操作紧接着跳过if()
块中的代码。因此,只需要两个分支指令,而不是为了将其减少到一个而必须跳过的各种麻烦。更进一步-这些cmp
和随后的分支通常会macro-fuse成为一个单独的操作,其成本与仅比较相同(并且需要一个周期)。虽然存在各种警告,但总体假设“在第二次测试中进行分支”需要很长时间可能并不牢固。(variable == SOME_CONSTANT || variable == OTHER_CONSTANT)
中,variable
有50%的时间等于SOME_CONSTANT
,49%的时间等于OTHER_CONSTANT
。因此,if
将被执行99%的时间,但第一个检查variable == SOME_CONSTANT
将是完全不可预测的:正好一半的时间进行分支!在这种情况下,即使有一些代价,合并检查也是一个好主意,因为错误预测是昂贵的。SOME_CONSTANT
为2,OTHER_CONSTANT
为4:void test(int a) {
if (a == 2 || a == 4) {
call();
}
}
无论是 clang
还是 icc
,都将其实现为两个检查和两个分支的系列,但最近的 gcc
使用 另一种技巧:
test(int, int):
sub edi, 2
and edi, -3
je .L4
rep ret
.L4:
jmp call()
a
中减去2,然后检查是否设置了除0b10以外的任何位。该检查仅接受值2和4。有趣的变换!对于可预测的输入,它并没有比两个分支方法更好,但对于“不可预测的子句但可预测的最终结果”情况,它将是一个巨大的胜利。1 在具有快速cmov
的平台上,使用两个cmov来实现||
可能不是一个糟糕的选择,而&&
可以类似地实现。