使用Microsoft编译器生成 CMOV 指令
Generating CMOV instructions using Microsoft compilers
为了在运行Windows 7 Pro的英特尔酷睿2上勉强完成一些cmov指令,我编写了下面的代码。它所做的只是从控制台获取一个字符串作为输入,应用一些移位操作来生成一个随机种子,然后将该种子传递给 srand,以生成一小数组伪随机数。然后评估伪随机数是否满足谓词函数(更任意的位洗牌),并输出"*"或"_"。实验的目的是生成 cmov 指令,但正如您在下面的反汇编中看到的那样,没有。
关于如何更改代码或 cflags 以便生成它们的任何提示?
#include <iostream>
#include <algorithm>
#include <string>
#include <cstdlib>
bool blackBoxPredicate( const unsigned int& ubref ) {
return ((ubref << 6) ^ (ubref >> 2) ^ (~ubref << 2)) % 15 == 0;
}
int main() {
const unsigned int NUM_RINTS = 32;
unsigned int randomSeed = 1;
unsigned int popCount = 0;
unsigned int * rintArray = new unsigned int[NUM_RINTS];
std::string userString;
std::cout << "input a string to use as a random seed: ";
std::cin >> userString;
std::for_each(
userString.begin(),
userString.end(),
[&randomSeed] (char c) {
randomSeed = (randomSeed * c) ^ (randomSeed << (c % 7));
});
std::cout << "seed computed: " << randomSeed << std::endl;
srand(randomSeed);
for( int i = 0; i < NUM_RINTS; ++i ) {
rintArray[i] = static_cast<unsigned int> (rand());
bool pr = blackBoxPredicate(rintArray[i]);
popCount = (pr) ? (popCount+1) : (popCount);
std::cout << ((pr) ? ('*') : ('_')) << " ";
}
std::cout << std::endl;
delete rintArray;
return 0;
}
并使用此生成文件来构建它:
OUT=cmov_test.exe
ASM_OUT=cmov_test.asm
OBJ_OUT=cmov_test.obj
SRC=cmov_test.cpp
THIS=makefile
CXXFLAGS=/nologo /EHsc /arch:SSE2 /Ox /W3
$(OUT): $(SRC) $(THIS)
cl $(SRC) $(CXXFLAGS) /FAscu /Fo$(OBJ_OUT) /Fa$(ASM_OUT) /Fe$(OUT)
clean:
erase $(OUT) $(ASM_OUT) $(OBJ_OUT)
然而,当我去查看是否生成了任何程序集时,我看到 Microsoft 的编译器为最后一个 for 循环生成了以下程序集:
; 34 : popCount = (pr) ? (popCount+1) : (popCount);
; 35 :
; 36 : std::cout << ((pr) ? ('*') : ('_')) << " ";
00145 68 00 00 00 00 push OFFSET $SG30347
0014a 85 d2 test edx, edx
0014c 0f 94 c0 sete al
0014f f6 d8 neg al
00151 1a c0 sbb al, al
00153 24 cb and al, -53 ; ffffffcbH
00155 04 5f add al, 95 ; 0000005fH
00157 0f b6 d0 movzx edx, al
0015a 52 push edx
0015b 68 00 00 00 00 push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
00160 e8 00 00 00 00 call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@D@Z ; std::operator<<<std::char_traits<char> >
00165 83 c4 08 add esp, 8
00168 50 push eax
00169 e8 00 00 00 00 call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
0016e 46 inc esi
0016f 83 c4 08 add esp, 8
00172 83 fe 20 cmp esi, 32 ; 00000020H
00175 72 a9 jb SHORT $LL3@main
供您参考,这是我的 cpu id 字符串和编译器版本。
PROCESSOR_ARCHITECTURE=x86
PROCESSOR_IDENTIFIER=x86 Family 6 Model 58 Stepping 9, GenuineIntel
PROCESSOR_LEVEL=6
PROCESSOR_REVISION=3a09
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
让Microsoft的 32 位 C/C++ 编译器发出CMOVcc
指令是非常困难的,如果不是完全不可能的话。
您必须记住的是,条件移动最初是在奔腾 Pro 处理器中引入的,虽然Microsoft有一个编译器开关可以调整为这个第 6 代处理器(早已弃用的/G6
)生成的代码,但它们从未发出专门在此处理器上运行的代码。代码仍然需要在第五代处理器(即奔腾和AMD K6)上运行,因此它不能使用CMOVcc
指令,因为这些指令会产生非法指令异常。与英特尔的编译器不同,全局动态调度没有(现在仍然没有)实现。
此外,值得注意的是,从未引入专门针对第 6 代处理器及更高版本的交换机。没有/arch:CMOV
或任何他们可能称之为它的东西。/arch
开关支持的值直接从IA32
(最低公分母,CMOV
可能是非法的)到SSE
。但是,该文档确实确认,正如人们所期望的那样,启用 SSE 或 SSE2 代码生成隐式允许使用条件移动指令和在 SSE之前引入的任何其他内容:
,编译器还使用支持 SSE 和 SSE2 的处理器修订版上存在的其他指令。一个例子是首次出现在英特尔处理器奔腾 Pro 修订版上的 CMOV 指令。
因此,为了有希望让编译器发出CMOV
指令,您必须设置/arch:SSE
或更高。当然,如今,这没什么大不了的。您可以简单地设置/arch:SSE
或/arch:SSE2
并且安全,因为所有现代处理器都支持这些指令集。
但这只是成功的一半。即使启用了正确的编译器开关,也很难让 MSVC 发出CMOV
指令。以下是两个重要的观察结果:
MSVC 10 (Visual Studio 2010) 及更早版本几乎从不生成
CMOV
指令。我从未在输出中看到过它们,无论我尝试了多少源代码变体。我说"虚拟"是因为可能有一些疯狂的边缘情况我错过了,但我非常怀疑。没有任何优化标志对此有任何影响。但是,MSVC 11(Visual Studio 2012)至少在这方面对代码生成器进行了重大改进。编译器的这个版本和更高版本现在似乎至少知道
CMOVcc
指令的存在,并且可以在正确的条件下发出它们(即,/arch:SSE
或更高版本,并使用条件运算符,如下所述)。我发现哄骗编译器发出
CMOV
指令的最有效方法是使用条件运算符而不是长格式的if
-else
语句。尽管就代码生成器而言,这两个构造应该是完全等效的,但它们不是。换句话说,虽然您可能会看到以下内容已转换为无分支
CMOVLE
指令:int value = (a < b) ? a : b;
您将始终获得以下序列的分支代码:
int value; if (a < b) value = a; else value = b;
至少,即使您使用条件运算符不会导致
CMOV
指令(例如在 MSVC 10 或更早版本上),您仍然可能很幸运地通过其他方式获得无分支代码 -例如,SETcc
或巧妙地使用SBB
和NEG
/NOT
/INC
/DEC
。这就是您在问题中显示的反汇编所使用的,虽然它不如CMOVcc
最佳,但它肯定是可比的,差异不值得担心。(唯一的其他分支指令是循环的一部分。
如果你真的想要无分支代码(你在手动优化时经常这样做),并且你没有运气让编译器生成你想要的代码,你需要更聪明地编写源代码。我很幸运地编写了使用按位或算术运算符无分支计算结果的代码。
例如,您可能希望以下函数生成最佳代码:
int Minimum(int a, int b)
{
return (a < b) ? a : b;
}
您遵循了规则 #2 并使用了条件运算符,但如果您使用的是旧版本的编译器,则无论如何都会获得分支代码。使用经典技巧智胜编译器:
int Minimum_Optimized(int a, int b)
{
return (b + ((a - b) & -(a < b)));
}
生成的目标代码不是完全最佳的(它包含一个冗余的CMP
指令,因为SUB
已经设置了标志),但它是无分支的,因此仍然比最初尝试随机输入导致分支预测失败要快得多。
再举一个例子,假设您要确定 32 位应用程序中的 64 位整数是否为负数。编写以下不言而喻的代码:
bool IsNegative(int64_t value)
{
return (value < 0);
}
并且会发现自己对结果感到非常失望。GCC 和 Clang 对此进行了合理的优化,但 MSVC 吐出了一个令人讨厌的条件分支。(不可移植的)技巧是意识到符号位位于高 32 位,因此您可以使用按位操作显式隔离和测试它:
bool IsNegative_Optimized(int64_t value)
{
return (static_cast<int32_t>((value & 0xFFFFFFFF00000000ULL) >> 32) < 0);
}
此外,其中一位评论员建议使用内联程序集。虽然这是可能的(Microsoft的 32 位编译器支持内联汇编),但它通常是一个糟糕的选择。内联程序集以相当显著的方式破坏优化器,因此除非您在内联程序集中编写大量代码,否则不太可能有实质性的净性能提升。此外,Microsoft的内联程序集语法非常有限。它在很大程度上牺牲了灵活性和简单性。特别是,无法指定输入值,因此您只能将输入从内存加载到寄存器中,并且调用方被迫将输入从寄存器溢出到内存以准备。这创造了一种现象,我喜欢称之为"一大堆洗牌'goin'on",或者简称为"慢代码"。在可以接受慢速代码的情况下,不要下降到内联程序集。因此,最好(至少在 MSVC 上)弄清楚如何编写 C/C++ 源代码来说服编译器发出您想要的目标代码。即使您只能接近理想的输出,这仍然比您使用内联装配所付出的代价要好得多。
请注意,如果以 x86-64 为目标,则不需要这些扭曲。Microsoft的 64 位 C/C++ 编译器在尽可能使用CMOVcc
指令方面明显更加积极,即使是旧版本也是如此。正如这篇博客文章所解释的,与 Visual Studio 2010 捆绑在一起的 x64 编译器包含许多代码质量改进,包括更好地识别和使用CMOV
指令。
此处不需要特殊的编译器标志或其他注意事项,因为支持 64 位模式的所有处理器都支持条件移动。我想这就是为什么他们能够为 64 位编译器提供正确的原因。我还怀疑在VS 2010中对x86-64编译器所做的一些更改被移植到VS 2012中的x86-32编译器,这解释了为什么它至少知道CMOV
的存在,但它仍然没有像64位编译器那样积极地使用它。
底线是,当面向 x86-64 时,以最有意义的方式编写代码。优化器实际上知道如何完成它的工作!
- 使用C++库在Android项目中修改gradle中的cmake参数,用于插入指令的测试
- 无法编译 rtmidi 测试 cmidiin.cpp 文件, 非法指令
- C++:对不存在的命名空间使用命名空间指令
- 函数名是c中该函数的第一条指令的地址吗
- 错误:无效的预处理指令 #i 的意思是 #if?
- 组装指令中乘法的下部和上部是什么
- OpenMP 与有序和关键指令并行
- C++中的移动分配出现问题.非法指令: 4.
- 嵌套命名空间的"using"指令,但需要命名内部命名空间
- C++CMake编译指令与
- 使用宏扩展的泛型:为什么指令缓存使用不当?
- 如何在 c++ 中确定一条指令(以字节为单位)在哪里结束,另一条指令从哪里开始?
- AVX 指令中寄存器和指针之间的客观差异
- while 循环 c++ 中的非法指令
- 如何在编译时定义C++预处理器指令的值?
- 存储指令是否会阻止缓存未命中的后续指令?
- 保证编译器指令在C++中重新排序
- VS2008中的AVX-512指令库
- 令人困惑的定义指令在C ++项目中
- 使用Microsoft编译器生成 CMOV 指令