当前位置: 首页 > 知识库问答 >
问题:

使用Microsoft编译器生成CMOV指令

颛孙森
2023-03-14

为了在运行windows 7 pro的intel core 2上完成一些cmov指令,我编写了以下代码。它所做的就是从控制台获取一个字符串作为输入,应用一些移位操作来生成一个随机种子,然后将该种子传递给srand,以生成一个小的伪随机数数组。然后评估伪随机数是否满足谓词函数(更任意的位随机),并输出“*”或“\u1”。实验的目的是生成cmov指令,但正如您在下面的反汇编中所看到的,没有。

有没有关于如何更改代码或cflag以便生成它们的提示?

#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)

然而,当我去查看是否生成了任何程序集时,我看到微软的编译器为最后一个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

共有1个答案

姚高爽
2023-03-14

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

您必须记住的是,条件移动最初是在奔腾Pro处理器中引入的,虽然Microsoft有一个编译器开关,可以调整此第6代处理器(长期不受欢迎的G6)的生成代码,但它们从未发出只在此处理器上运行的代码。代码仍然需要在第五代处理器(即奔腾和AMD K6)上运行,因此无法使用CMOVcc指令,因为这些指令会产生非法的指令异常。与英特尔编译器不同,全局动态调度没有(现在仍然没有)实现。

此外,值得注意的是,从未引入过专门针对第六代及更高版本处理器的开关。没有/arch: CMOV或他们可能称之为的任何东西。/arch开关的支持值直接从IA32(最低公分母,CMOV可能是非法的)到SSE。但是,留档确实确认,正如人们所料,启用SSE或SSE2代码生成隐式启用条件移动指令和SSE之前引入的任何其他内容:

除了使用SSE和SSE2指令外,编译器还使用支持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-ore语句。尽管就代码生成器而言,这两个构造应该完全等价,但它们不是。

    换句话说,虽然您可能会看到以下内容被转换为无分支的CMOVLE指令:

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

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

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

    至少,即使使用条件运算符不会导致CMOV指令(如在MSVC 10或更早版本上),您仍然有幸通过其他方式获得无分支代码,例如,SETcc或巧妙使用SBB和NEG。这就是您在问题中展示的反汇编所使用的,虽然它没有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);
    }
    

    此外,其中一位评论员建议使用内联汇编。虽然这是可能的(微软的32位编译器支持内联汇编),但它通常是一个糟糕的选择。内联汇编以相当大的方式扰乱了优化器,因此除非您在内联汇编中编写大量代码,否则不太可能获得显着的净性能提升。此外,微软的内联汇编语法非常有限。它在很大程度上以灵活性换取简单性。特别是,没有办法指定输入值,因此您无法将输入从内存加载到寄存器中,并且调用者被迫将输入从寄存器溢出到内存中以作准备。这就产生了一种现象,我喜欢称之为“一大堆混乱的‘继续’”,或者简称为“缓慢的代码”。在可以接受缓慢代码的情况下,您不会使用内联汇编。因此,最好(至少在MSVC上)弄清楚如何编写C/C源代码来说服编译器发出您想要的目标代码。即使您只能接近理想的输出,这仍然比您为使用内联汇编而付出的代价要好得多。

    请注意,如果以x86-64为目标,则无需进行任何这些扭曲。微软的64位C/C编译器在尽可能使用CMOVcc指令方面,甚至在旧版本上,都表现得更加积极。正如这篇博文所解释的,与Visual Studio 2010捆绑的x64编译器包含许多代码质量改进,包括更好地识别和使用CMOV指令。

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

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

  •  类似资料:
    • 有没有办法让Clang、GCC或VS仅使用标准-C(98/11/14)生成adc(带进位添加)指令?(编辑:我的意思是在x64模式下,如果不清楚,对不起。)

    • 我的分析器已将以下函数分析确定为热点。 特别是一条汇编指令MOVZX(零扩展移动)负责运行时的大部分。if语句编译为 我想诱使编译器不生成这条指令,但我想我首先需要了解为什么会生成这条指令。考虑到我正在使用相同的数据类型,为什么要使用加宽/零扩展? (在godbolt编译器资源管理器上找到整个函数。)

    • 问题内容: 请帮助我,如何使AngularJS编译指令生成的代码? 您甚至可以在这里找到相同的代码,http://jsbin.com/obuqip/4/edit 的HTML Java脚本 问题答案: 这是一个既不使用编译功能也不使用链接功能的版本: 请注意,模板被包装在中,因为模板需要具有一个根元素。(如果没有,它将有两个 根元素。) 需要对HTML进行少许修改以进行插值: 小提琴。

    • 问题内容: 可以说,我的Java程序的瓶颈确实是一些紧密的循环,无法计算一堆矢量点积。是的,我已经进行了概要分析,是的,它是瓶颈,是的,它很重要,是的,这就是算法的方式,是的,我运行了Proguard来优化字节码,等等。 实质上,这是点产品。与之类似,我有两个,我需要计算成对乘积之和。我知道处理器指令集可以像SSE或MMX一样快速且批量地执行此类操作。 是的,我可能可以通过在JNI中编写一些本机代

    • 使用命令行编译器 注解 这一节并不适用于 solcjs solc 是 Solidity 源码库的构建目标之一,它是 Solidity 的命令行编译器。你可使用 solc --help 命令来查看它的所有选项的解释。该编译器可以生成各种输出,范围从简单的二进制文件、汇编文件到用于估计“gas”使用情况的抽象语法树(解析树)。如果你只想编译一个文件,你可以运行 solc --bin sourceFil

    • 工具链介绍 gxnpuc 用于把模型文件编译成能在 NPU 上运行的 npu 文件。 usage: gxnpuc [-h] [-V] [-L] [-v] [-m] [-c CMD [CMD ...]] [config_filename] NPU Compiler positional arguments: config_filename config file optiona