为什么MSVC在执行此位测试之前会发出无用的MOVSX
Why does MSVC emit a useless MOVSX before performing this Bit Test?
在MSVC 2013中编译以下代码,64位版本构建,/O2
优化:
while (*s == ' ' || *s == ',' || *s == 'r' || *s == 'n') {
++s;
}
我得到了以下代码,它使用64位寄存器作为带有bt
(位测试)指令的查找表,进行了非常酷的优化。
mov rcx, 17596481020928 ; 0000100100002400H
npad 5
$LL82@myFunc:
movzx eax, BYTE PTR [rsi]
cmp al, 44 ; 0000002cH
ja SHORT $LN81@myFunc
movsx rax, al
bt rcx, rax
jae SHORT $LN81@myFunc
inc rsi
jmp SHORT $LL82@myFunc
$LN81@myFunc:
; code after loop...
但我的问题是:movsx rax, al
在第一个分支之后的目的是什么?
首先,我们将字符串中的一个字节加载到rax
中,并对其进行零扩展:
movzx eax, BYTE PTR [rsi]
然后,cmp
/ja
对在al
和44
之间执行无符号比较,如果al
较大,则向前分支。
现在,我们知道了0 <= al <= 44
的无符号数。因此,al
的最高比特不可能被设置!
尽管如此,下一条指令是movsx rax, al
。这是一个延伸动作的标志。但自:
al
是rax
的最低字节- 我们已经知道
rax
的其他7个字节为零 - 我们刚刚证明了
al
的最高比特不可能被设置
该CCD_ 17必须是no-op。
MSVC为什么这么做?我假设这不是为了填充,因为在这种情况下,另一个npad
会使含义更清晰。是刷新数据依赖项还是其他什么?
(顺便说一句,这个bt
优化真的让我很高兴。一些有趣的事实:它运行的时间是你所期望的4对cmp
/je
的0.6倍,它比strspn
或std::string::find_first_not_of
快,而且它只发生在64位构建中,即使感兴趣的字符的值低于32。)
你肯定知道这个优化由优化器中查找模式的特定代码生成。只是比特掩码的生成就泄露了它。是的,好把戏。
这里有两个基本的代码生成案例。第一个是更通用的,其中(charmax-charmin<=64)但charmax>=64。优化器需要从您看到的代码中生成不同的代码,它需要减去charmin。该版本的没有MOVSX指令。您可以通过将*s == ' '
替换为*s == 'A'
来查看它。
然后是您测试的特殊情况,所有要测试的字符代码恰好是<64.微软程序员确实在他的代码中处理了这个问题,他确保不会生成愚蠢的SUBEAX,0指令。但是忽略了生成MOVSX是不必要的。在一般情况下,只检查最佳代码肯定会错过。代码中的一个通用函数调用很容易被忽略,请注意使用/J编译时指令是如何更改为MOVZX的。否则很容易被认为是必要的,没有BT指令将8位寄存器作为第二个操作数,因此AL寄存器的负载本身不够。
可能存在一个假设的后优化器,用于优化优化器生成的优化代码。并决定保留MOVSX以改进超标量执行。我严重怀疑它的存在。
正如Hans-Passant已经提到的,您的代码是优化的特殊情况。我没有查看编译器/优化器的来源,所以我只能说我预计会发生什么。然而,以下是我对此的解释。
您在C/C++中的代码:
while (*s == ' ' || *s == ',' || *s == 'r' || *s == 'n') {
++s;
}
相当于:
while (*s == 32 || *s == 44 || *s == 13 || *s == 12) {
++s;
}
或:
while (*s == 12 || *s == 13 || *s == 32 || *s == 44) {
++s;
}
优化器检测到存在同一字符的多次(>=3次)比较的"if"。现在,优化器根据需要生成尽可能多的64位模式(对于8位字符,最多4个模式,64*4=>所有256个可能的值)。每个比特模式都有一个偏移量(=模式中第一个比特对应的测试值),需要从测试值中减去。在您的情况下,对于范围从12到44的值,只需要一个64位模式。所以你的代码会被转换成一些通用代码,比如:
#define ranged_bittest(pattern, value, min_val, max_val)
(val >= min_val) && (val <= max_val) && BT_with_offset(pattern, val, min_val)
while ( !ranged_bittest(<BITPATTERN>, *s, 12, 44) ) {
++s;
}
现在,一些其他优化采用ranged_bittest(<BITPATTERN>, *s, 12, 44);
,因为它检测到具有起始偏移12的位测试和可以安全地向左移位12位的模式(因为模式的高12位为零)。ranged_bittest(<BITPATTERN>, *s, 12, 44)
=>ranged_bittest(<BITPATTERN> << 12, *s, 0, 44)
这演变为:
while (!(val >= 0 && val <= 44 && BT_with_offset(<SHIFTEDBITPATTERN>, *s, 0))) {
++s;
}
val >= 0 &&
被优化掉了(因为它被认为总是真的),"while"被翻译成一些带有中断的"do"循环;(在大多数情况下,这对分支预测更好)导致:
do {
if (val > 44) break;
if (BT_with_offset(<SHIFTEDBITPATTERN>, *s, 0)) break;
++s;
} while (true);
无论出于何种原因,优化都会在此停止(例如,由于性能原因,对代码片段应用进一步优化的频率有限制)。
现在正在组装生产线
if (BT_with_offset(<SHIFTEDBITPATTERN>, *s, 0)) break;`
被翻译成类似于:
MOV <reg64_A>, const_pattern
MOV <reg64_B>, value
SUB <reg64_B>, const_offset
BT <reg64_A>, <reg64_B>
装配优化器减少:
MOV <reg64_B>, value
SUB <reg64_B>, 0
仅
MOVSX <reg64_B>, value
在您的特殊情况下,这是:
MOVSX rax, al
程序集优化器(不知何故)没有足够的信息来完全消除符号扩展。也许程序集优化器相当"愚蠢"(不能做任何逻辑表达式优化之类的事情,只是简单的替换),或者完整的优化级别还没有激活。
因此,它不能删除movsx rax,al
,因为在代码的这一点上,它没有任何关于rax
或al
的可能值的元信息。
我不知道,如果这是真的,但这是我对这个案件的最佳猜测。。。
当我第一次看到这段代码时,最让我印象深刻的是它的优化效果有多差
是的,使用64位寄存器作为查找表是一个巧妙的技巧,但是。。。
- 为什么仍然使用
INC
而不是更好的ADD,1
- 如果BT指令的目标操作数是寄存器,那么当我们知道BT指令使用其源操作数MOD 64时,为什么要使用
CMP ...
JA ...
MOVSX ...
(因此有58位不需要考虑) - 为什么不从条件分支被预测会倒退这一事实中受益呢
- 为什么不减少分支机构的数量
我认为一个真正的汇编程序程序员会写得更像这样:
mov rcx, 0FFFFEFFEFFFFDBFFh ;~0000100100002400h
sub rsi, 1
npad 1
$LL82@myFunc:
add rsi, 1
movzx eax, BYTE PTR [rsi] ;mov al,[rsi]
test al, 11000000b
setz bl
test bl, bl
bt rcx, rax
ja SHORT $LL82@myFunc
如果(CF或ZF)=0 ,则ja
跳转
- ZF=1表示AL=[64255],不关心CF
- 对于AL=[0.63],ZF=0;对于AL={10,13,32,44},CF=0
对于[64255]范围内的所有ASCII,test al, 11000000b
指令将给出非零结果(ZF=0)。因为组合setz bl
test bl, bl
随后用于将ZF翻转为1,所以ja
指令不再有任何机会继续循环
相反,对于[0.63]范围内的所有ASCII,ZF最终将为0,从而允许ja
完全解释从bt rcx, rax
指令获得的CF。
也许我们对优化编译器抱有很大期望
- 识别并跟踪无用的复制
- 抑制非平凡无用的警告"control may reach end of non-void function"
- 为什么C++中没有无用的条件回报警告
- decltype(function_name) 的返回类型是完全无用的
- 返回无用对象的元组
- 直接 X 12 示例代码中看似无用C++行
- 为什么 GCC 在使用继承的构造函数时警告我无用的强制转换
- 特征:沿着一个维度复制项目,而无用分配
- GDB 在使用核心转储时提供无用的回溯
- Eclipse CDT-输出 - 使用VS编译器时无用
- Ifstream 读取无用的数据
- 当我需要添加一个无用的返回语句时,C++约定是什么
- G++删除了无用的功能?未定义的引用
- std::generic_category() 是无用的
- sprintf - 字符串前 4 个无用的 ASCII 字符
- 当将指针返回到数据时,C++是无用的关键部分
- 缺少循环会使代码无用
- 为什么MSVC在执行此位测试之前会发出无用的MOVSX
- 无用的垃圾在谷歌breakpad
- C++无用友函数声明