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

编译器生成代价高昂的MOVZX指令

帅博远
2023-03-14

我的分析器已将以下函数分析确定为热点。

typedef unsigned short ushort;

bool isInteriorTo( const std::vector<ushort>& point , const ushort* coord , const ushort dim )
{
    for( unsigned i = 0; i < dim; ++i )
    {
        if( point[i + 1] >= coord[i] ) return false;
    }

    return true;  
}

特别是一条汇编指令MOVZX(零扩展移动)负责运行时的大部分。if语句编译为

mov     rcx, QWORD PTR [rdi]
lea     r8d, [rax+1]
add     rsi, 2
movzx   r9d, WORD PTR [rsi-2]
mov     rax, r8
cmp     WORD PTR [rcx+r8*2], r9w
jae     .L5

我想诱使编译器不生成这条指令,但我想我首先需要了解为什么会生成这条指令。考虑到我正在使用相同的数据类型,为什么要使用加宽/零扩展?

(在godbolt编译器资源管理器上找到整个函数。)

共有2个答案

长孙深
2023-03-14

movzx指令零将量扩展到较大的寄存器中。在您的例子中,一个字(两个字节)被零扩展为dword(四个字节)。零扩展本身通常是自由的,缓慢的部分是从RAM加载内存操作数。

为了加快速度,您可以尝试确保要从RAM中获取的数据在需要时位于L1缓存中。您可以通过将策略预取内在函数放置到适当的位置来做到这一点。例如,假设一个缓存行是64字节,您可以在每次循环时添加一个预取内在函数来获取数组条目i 32

您还可以考虑改进算法,减少从内存中提取的数据,但这似乎不太可能。

刘修能
2023-03-14

谢谢你的好问题!

英特尔®64和IA-32架构优化参考手册,第3.5.1.8节:

修改部分寄存器的代码序列可能会在其依赖链中经历一些延迟,但可以通过使用依赖断开习惯用法来避免。在基于英特尔酷睿微架构的处理器中,当软件使用这些指令将寄存器内容清除为零时,许多指令可以帮助清除执行依赖。通过在32位寄存器而不是部分寄存器上操作来打破指令之间对寄存器部分的依赖。对于移动,这可以通过32位移动或使用MOVZX来实现。

汇编/编译器编码规则37。(M impact,MH通用性):通过对32位寄存器而不是部分寄存器进行操作,打破指令之间对寄存器部分的依赖。对于移动,这可以通过32位移动或使用MOVZX来实现。

编译器知道movzx并不昂贵,并尽可能经常地使用它。编码movzx可能比mov需要更多的字节,但执行起来并不昂贵。

逻辑相反,使用movzx(填充整个寄存器)的程序实际上比仅使用mov(仅设置寄存器的较低部分)的程序运行得更快。

让我在下面的代码片段中向您演示这个结论。它是使用N切片算法实现CRC-32计算的代码的一部分。这是:

    movzx   ecx, bl
    shr     ebx, 8
    mov     eax, dword ptr [ecx * 4 + edi + 1024 * 3]

    movzx   ecx, bl
    shr     ebx, 8
    xor     eax, dword ptr [ecx * 4 + edi + 1024 * 2]

    movzx   ecx, bl
    shr     ebx, 8
    xor     eax, dword ptr [ecx * 4 + edi + 1024 * 1]
    
    skipped 6 more similar triplets that do movzx, shr, xor.
    
    dec     <<<a counter register >>>>
    jnz     …… <<repeat the whole loop again>>>

这是第二个代码片段。我们已经提前清除了ecx,现在只是将“movzx ecx,bl”改为“mov cl,bl”:

    // ecx is already cleared here to 0

    mov     cl, bl
    shr     ebx, 8
    mov     eax, dword ptr [ecx * 4 + edi + 1024 * 3]

    mov     cl, bl
    shr     ebx, 8
    xor     eax, dword ptr [ecx * 4 + edi + 1024 * 2]

    mov     cl, bl
    shr     ebx, 8
    xor     eax, dword ptr [ecx * 4 + edi + 1024 * 1]
    
    <<< and so on – as in the example #1>>>

现在猜猜上面两段代码中哪一段运行得更快?您之前是否认为速度是相同的,或者movzx版本更慢?事实上,movzx代码更快,因为自奔腾Pro以来的所有CPU都无序执行指令和寄存器重命名。

寄存器重命名是CPU内部使用的一种技术,它消除了由于连续指令重用寄存器而产生的虚假数据依赖关系,这些指令之间没有任何真实的数据依赖关系。

让我从第一个代码片段中获取前4条指令:

  1.     movzx   ecx, bl
    
        shr     ebx, 8
    
        mov     eax, dword ptr [ecx * 4 + edi + 1024 * 3]
    
        movzx   ecx, bl
    

    如您所见,指令4依赖于指令2。指令4不依赖于指令3的结果。

    因此,CPU可以并行(一起)执行指令3和4,但指令3使用由指令4修改的寄存器(只读),因此指令4只能在指令3完全完成后开始执行。然后,让我们在第一个三元组之后将寄存器ecx重命名为edx,以避免这种依赖关系:

        movzx   ecx, bl
        shr     ebx, 8
        mov     eax, dword ptr [ecx * 4 + edi + 1024 * 3]
    
        movzx   edx, bl
        shr     ebx, 8
        xor     eax, dword ptr [edx * 4 + edi + 1024 * 2]
    
        movzx   ecx, bl
        shr     ebx, 8
        xor     eax, dword ptr [ecx * 4 + edi + 1024 * 1]
    

    以下是我们现在拥有的:

    1.     movzx   ecx, bl
      
          shr     ebx, 8
      
          mov     eax, dword ptr [ecx * 4 + edi + 1024 * 3]
      
          movzx   edx, bl
      

      现在,指令4决不使用指令3所需的任何寄存器,反之亦然,因此指令3和4肯定可以同时执行!

      这就是中央处理器为我们所做的。中央处理器在将指令转换为无序算法将执行的微操作(micro-ops)时,在内部重命名寄存器以消除这些依赖关系,因此微操作处理重命名的内部寄存器,而不是我们所知道的真实寄存器。因此,我们不需要像我在上面的例子中刚刚重命名的那样重命名寄存器——中央处理器将自动为我们重命名所有内容,同时将指令转换为微操作。

      指令3和指令4的微操作将并行执行,因为指令4的微操作将处理与指令3的微操作完全不同的内部寄存器(作为ecx暴露在外),因此我们不需要重命名任何东西。

      让我将代码还原为初始版本。这是:

      1.     movzx   ecx, bl
        
            shr     ebx, 8
        
            mov     eax, dword ptr [ecx * 4 + edi + 1024 * 3]
        
            movzx   ecx, bl
        

        (指令3和指令4并行运行,因为指令3的ecx不是指令4的ecx,而是一个不同的重命名寄存器——中央处理器自动从内部可用寄存器池中为指令4微操作分配了一个新的新寄存器)。

        现在让我们回到movxz vs mov。

        Movzx完全清除寄存器,因此CPU肯定知道我们不依赖于保留在寄存器高位的任何先前值。当CPU看到movxz指令时,它知道可以在内部安全地重命名寄存器,并与以前的指令并行执行该指令。现在,从我们的示例#2中取前四条指令,其中我们使用mov而不是movzx:

        1.    mov     cl, bl
          
             shr     ebx, 8
          
             mov     eax, dword ptr [ecx * 4 + edi + 1024 * 3]
          
             mov     cl, bl
          

          在这种情况下,指令4通过修改cl,修改了ecx的第0-7位,使第8-32位保持不变。因此,CPU不能仅仅为指令4重命名寄存器,并分配另一个新的寄存器,因为指令4依赖于先前指令留下的第8-32位。CPU必须在执行指令4之前保留第8-32位。因此,它不能仅仅重命名寄存器。它将等到指令3完成后再执行指令4。指令4并没有变得完全独立——它取决于ECX的先前值和bl的先前值。因此,它同时依赖于两个寄存器。如果我们使用movzx,它将只依赖于一个寄存器-bl。因此,指令3和指令4不会并行运行,因为它们相互依赖。悲伤但真实。

          这就是为什么操作完整寄存器总是更快。假设我们只需要修改寄存器的一部分。在这种情况下,改变整个寄存器总是更快(例如,使用movzx)——让中央处理器确定寄存器不再依赖于其先前的值。修改完整寄存器允许中央处理器重命名寄存器,让乱序执行算法与其他指令一起执行这条指令,而不是一个接一个地执行它们。

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

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

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

  • 我在这里发现https://stackoverflow.com/a/12901159/6655884一个函数调用,它用附加线程,执行调用,然后用分离它。 我想知道这个过程是否代价高昂。我有一个C函数 它将被几个C线程调用,以将事件发送到Java端,因此我不能简单地附加一个线程,而永远不会分离它,因为许多不同的线程将调用。所以我想知道,每次调用调用,调用Java,然后调用,是否代价高昂。如果是,我应

  • 至少在GCC中,如果我们提供生成汇编代码的选项,编译器会通过创建一个包含汇编代码的文件来服从。但是,当我们简单地运行命令而没有任何选项时,它不会在内部生成汇编代码吗? 如果是,那么为什么它需要首先生成一个汇编代码,然后将其翻译成机器语言?

  • 我使用了Jooq官方网站上的这个工具:https://github.com/etiennestuder/gradle-jooq-plugin从我的数据库生成代码。 但如果我设置