c++ - 编译器在这里做了什么,允许通过很少的实际比较来比较许多值?
问题描述
我的问题是关于编译器在这种情况下所做的优化代码的方式比我认为可能的方式更多。
鉴于此枚举:
enum MyEnum {
Entry1,
Entry2,
... // Entry3..27 are the same, omitted for size.
Entry28,
Entry29
};
而这个功能:
bool MyFunction(MyEnum e)
{
if (
e == MyEnum::Entry1 ||
e == MyEnum::Entry3 ||
e == MyEnum::Entry8 ||
e == MyEnum::Entry14 ||
e == MyEnum::Entry15 ||
e == MyEnum::Entry18 ||
e == MyEnum::Entry21 ||
e == MyEnum::Entry22 ||
e == MyEnum::Entry25)
{
return true;
}
return false;
}
对于该函数,MSVC 在使用 -Ox 优化标志 ( Godbolt )编译时生成此程序集:
bool MyFunction(MyEnum) PROC ; MyFunction
cmp ecx, 24
ja SHORT $LN5@MyFunction
mov eax, 20078725 ; 01326085H
bt eax, ecx
jae SHORT $LN5@MyFunction
mov al, 1
ret 0
$LN5@MyFunction:
xor al, al
ret 0
当使用 -O3 标志编译时,Clang 会生成类似的(稍微好一点,少一个跳转)程序集:
MyFunction(MyEnum): # @MyFunction(MyEnum)
cmp edi, 24
ja .LBB0_2
mov eax, 20078725
mov ecx, edi
shr eax, cl
and al, 1
ret
.LBB0_2:
xor eax, eax
ret
这里发生了什么?我看到即使我向函数添加更多枚举比较,生成的程序集实际上并没有变得“更多”,只是这个幻数(20078725)发生了变化。该数字取决于函数中发生了多少枚举比较。我不明白这里发生了什么。
我看这个的原因是我想知道编写上面的函数是否很好,或者像这样,通过按位比较:
bool MyFunction2(MyEnum e)
{
if (
e == MyEnum::Entry1 |
e == MyEnum::Entry3 |
e == MyEnum::Entry8 |
e == MyEnum::Entry14 |
e == MyEnum::Entry15 |
e == MyEnum::Entry18 |
e == MyEnum::Entry21 |
e == MyEnum::Entry22 |
e == MyEnum::Entry25)
{
return true;
}
return false;
}
这将导致使用 MSVC 生成此程序集:
bool MyFunction2(MyEnum) PROC ; MyFunction2
xor edx, edx
mov r9d, 1
cmp ecx, 24
mov eax, edx
mov r8d, edx
sete r8b
cmp ecx, 21
sete al
or r8d, eax
mov eax, edx
cmp ecx, 20
cmove r8d, r9d
cmp ecx, 17
sete al
or r8d, eax
mov eax, edx
cmp ecx, 14
cmove r8d, r9d
cmp ecx, 13
sete al
or r8d, eax
cmp ecx, 7
cmove r8d, r9d
cmp ecx, 2
sete dl
or r8d, edx
test ecx, ecx
cmove r8d, r9d
test r8d, r8d
setne al
ret 0
由于我不了解第一种情况会发生什么,因此我无法真正判断哪种情况对我来说更有效。
解决方案
相当聪明!与 24 的第一个比较是进行粗略的范围检查——如果大于 24 或小于 0,它将退出;这很重要,因为随后对幻数进行操作的指令对于操作数范围有一个硬上限 [0, 31]。
其余的,幻数只是一个位掩码,位对应于设置的“好”值。
>>> bin(20078725)
'0b1001100100110000010000101'
很容易发现第 1 位和第 3 位(从 1 到右数)集合,第 8、第 14、第 15、...
MSVC 使用 BT(位测试)指令和分支“直接”检查它,clang 将其移动适当的数量(以使相关位处于最低顺序位置)并保持它与零(避免分支) .
与 clang 版本对应的 C 代码将类似于:
bool MyFunction(MyEnum e) {
if(unsigned(e) > 24) return false;
return (20078725 >> e) & 1;
}
至于 MSVC 版本,它更像
inline bool bit_test(unsigned val, int bit) {
return val & (1<<bit);
}
bool MyFunction(MyEnum e) {
if(unsigned(e) > 24) return false;
return bit_test(20078725, e);
}
(我将bit_test
函数分开以强调它实际上是汇编中的一条指令,那个val & (1<<bit)
东西与原始汇编没有对应关系。
至于if
基于 - 的代码,它非常糟糕——它使用了大量的 CMOV 和 ORs 结果一起,这都是更长的代码,并且可能会序列化执行。我怀疑相应的clang代码会更好。OTOH,您使用按位 OR ( |
) 而不是语义上更正确的逻辑 OR ( ||
) 编写了此代码,并且编译器严格遵循您的命令(典型的 MSVC)。
另一种尝试的可能性可能是switch
- 但与已经为第一个片段生成的代码相比,我认为没有太多收获,这对我来说看起来相当不错。
好的,对所有编译器的所有版本进行快速测试,我们可以看到:
- 上面 CLang 输出的 C 翻译在所有编译器中产生几乎相同的代码(= 到 clang 输出);对于 MSVC 翻译也是如此;
- 按位或版本与 CLang 和 gcc 中的逻辑或版本(=好)相同;
- 一般来说,gcc 的作用与 CLang 基本相同,但
switch
情况除外; switch
结果五花八门:- 通过生成完全相同的代码,CLang 做得最好;
- gcc 和 MSVC 都生成基于跳转表的代码,在这种情况下不太好;然而:
- gcc 更喜欢发出一个 QWORD 表,交易大小以简化设置代码;
- 相反,MSVC 会发出一个 BYTE 表,并以设置代码大小进行支付;即使将 -O3 更改为 -Os(针对大小进行优化),我也无法让 gcc 发出类似的代码。
推荐阅读
- python - 如何获取修改对象的用户?django-简单历史
- python - 列表表达式或调试器问题:NameError:未定义名称
- javascript - 正则表达式 - 如何使用起始词和结束词匹配完整的段落(包括新行)
- html - 浮动标签在 Chrome 中不能像 mozilla firefox 一样工作
- flutter - 颤振底部导航栏在导航上消失
- python - 对以小写或大写开头的重音名称进行排序
- php - 错误:“无法打开图像”仅在 docker 容器中
- javascript - 隐含地具有“任何”类型,因为 typescript 中的类型表达式
- android - 颤振 | 如何存储列表列表
在共享首选项中? - microsoft-teams - manifest 和 iframe 源中定义的应用程序资源不匹配