使用Microsoft编译器生成CMOV指令

时间:2012-12-01 16:27:27

标签: c++ visual-c++ assembly x86 disassembly

为了在运行Windows 7 pro的intel core 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;
}

并使用此makefile构建它:

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)

但是当我去查看是否有任何生成时,我看到微软的编译器为最后一个循环生成了以下程序集:

; 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

1 个答案:

答案 0 :(得分:3)

要使Microsoft的32位C / C ++编译器发出CMOVcc指令,极其很难,如果不是完全不可能的话。

你必须记住的是,Pentium Pro处理器首次引入了条件移动,而微软有一个编译器开关,它将调整为第六代处理器生成的代码(不推荐使用/G6),它们从未发出在此处理器上以运行的代码。代码仍然需要在第5代处理器(,Pentium和AMD K6)上运行,因此它不能使用CMOVcc指令,因为这些指令会产生非法的指令异常。与英特尔的编译器不同,全局动态调度没有(现在仍未实施)。

此外,值得注意的是,没有任何开关被引入目标专用第六代处理器及以后。没有/arch:CMOV或他们可能称之为的任何东西。 /arch switch支持的值直接从IA32CMOV可能非法的最低公分母)到SSE。但是,the documentation 确实确认,正如人们所预料的那样,启用SSE或SSE2代码生成隐式地允许使用条件移动指令和之前引入的任何其他内容 SSE:

  

除了使用SSE和SSE2指令外,编译器还使用支持SSE和SSE2的处理器修订版中的其他指令。一个例子是首先出现在Intel处理器的Pentium Pro版本中的CMOV指令。

因此,为了让编译器发出CMOV指令,您必须设置/arch:SSE或更高版本。如今,当然,这没什么大不了的。您可以设置/arch:SSE/arch:SSE2并保持安全,因为所有现代处理器都支持这些指令集。

但这只是战斗的一半。即使启用了正确的编译器开关,也很难让MSVC发出CMOV指令。以下是两个重要的观察结果:

  1. MSVC 10(Visual Studio 2010)及更早版本从不生成CMOV指令。永远不会在输出中看到它们,无论我尝试了多少源代码变体。我说&#34;虚拟&#34;因为可能会有一些我错过的疯狂边缘案例,但我非常怀疑它。没有任何优化标志对此有任何影响。

    但是,MSVC 11(Visual Studio 2012)对代码生成器引入了重大改进,至少在这方面是这样。现在,这个和更高版本的编译器似乎至少意识到存在CMOVcc指令,并且可能在适当的条件下发出它们(< em> ie ,/arch:SSE或更高版本,并使用条件运算符,如下所述。)

  2. 我发现让编译器发出CMOV指令的最有效方法是使用条件运算符 而不是长格式的if - else语句。虽然就代码生成器而言,这两个结构应完全等效,但它们不是。

    换句话说,当你可能看到以下翻译成无分支CMOVLE指令时:

    int value = (a < b) ? a : b;
    

    您将始终获取以下序列的分支代码:

    int value;
    if (a < b)    value = a;
    else          value = b;
    

    至少,即使您使用条件运算符不会导致CMOV指令(例如在MSVC 10或更早版本上),您仍然可以通过以下方式获得无分支代码其他一些方法 - 例如SETcc或巧妙使用SBBNEG / NOT / INC / {{1} }。这就是您在问题中使用的反汇编所使用的内容,虽然它不像DEC那样最优,但它肯定是可比的而且差别不大值得担心的。 (唯一的其他分支指令是循环的一部分。)

  3. 如果您真的想要无分支代码(you often do when hand-optimizing),并且您没有运气让编译器生成您想要的代码,那么您需要更加聪明你写的源代码。我编写的代码使用按位或算术运算符无分支地计算结果,我有幸运。

    例如,您可能希望以下函数生成最佳代码:

    CMOVcc

    您遵循规则#2并使用了条件运算符,但如果您使用旧版本的编译器,则无论如何都会获得分支代码。使用经典技巧超越编译器:

    int Minimum(int a, int b)
    {
        return (a < b) ? a : b;
    }
    

    生成的目标代码不是最佳的(它包含一个冗余的int Minimum_Optimized(int a, int b) { return (b + ((a - b) & -(a < b))); } 指令,因为CMP已经设置了标志),但它是无分支的,因此仍然会比原始指令快得多尝试导致分支预测失败的随机输入。

    作为另一个示例,假设您要确定32位应用程序中的64位整数是否为负数。您编写以下不言而喻的代码:

    SUB

    并且会发现自己对结果非常失望。 GCC和Clang明智地优化了这一点,但是MSVC吐出了一个讨厌的条件分支。 (非便携式)技巧是实现符号位在高32位,因此您可以使用按位操作显式隔离和测试:

    bool IsNegative(int64_t value)
    {
        return (value < 0);
    }
    

    此外,其中一位评论员建议使用内联汇编。虽然这是可能的(Microsoft的32位编译器支持内联汇编),it is often a poor choice。内联汇编以相当重要的方式破坏优化器,因此除非您在内联汇编中编写重要的代码段,否则不太可能获得可观的净性能增益。此外,Microsoft的内联汇编语法非常有限。它在很大程度上简化了灵活性。特别是,无法指定输入值,因此您无法将输入从内存加载到寄存器中,并且调用者被强制将输入从寄存器溢出到内存中。制备。这就产生了一种我喜欢称之为的现象#34;一个完整的shufflin&#39;布莱恩&#39; on&#34;,或简称,&#34;慢代码&#34;。在可接受慢代码的情况下,您不会放弃内联汇编。因此,最好(至少在MSVC上)找出如何编写C / C ++源代码,以说服编译器发出所需的目标代码。即使您只能关闭到理想输出,这仍然比使用内联汇编所支付的罚款要好得多。

    请注意,如果您定位x86-64,则不需要这些扭曲。 微软的64位C / C ++编译器在尽可能使用bool IsNegative_Optimized(int64_t value) { return (static_cast<int32_t>((value & 0xFFFFFFFF00000000ULL) >> 32) < 0); } 指令时更加积极,甚至是旧版本。与this blog post explains一样,与Visual Studio 2010捆绑在一起的x64编译器包含许多代码质量改进,包括更好地识别和使用CMOVcc指令。

    此处不需要特殊的编译器标志或其他注意事项,因为支持64位模式的所有处理器都支持条件移动。我想这就是他们能够为64位编译器做到正确的原因。我还怀疑在VS 2010中对x86-64编译器所做的一些更改被移植到VS 2012中的x86-32编译器,解释了为什么它至少知道CMOV的存在,但它仍然没有像64位编译器那样积极地使用它。

    底线是,在定位x86-64时,以最有意义的方式编写代码。优化器实际上知道如何完成它的工作!