【问题标题】:Convert GCC Inline Assembly CMOV to Visual Studio Assembler将 GCC 内联汇编 CMOV 转换为 Visual Studio 汇编器
【发布时间】:2016-06-12 20:29:57
【问题描述】:

Linear vs. Binary Search 文章中,有一个使用 CMOV 指令的二分查找的快速实现。我想在 VC++ 中实现这一点,因为我正在处理的应用程序依赖于二进制搜索的性能。

该实现有一些 GCC 内联汇编器,声明如下:

static int binary_cmov (const int *arr, int n, int key) {
    int min = 0, max = n;
    while (min < max) {
            int middle = (min + max) >> 1;

            asm ("cmpl %3, %2\n\tcmovg %4, %0\n\tcmovle %5, %1"
                 : "+r" (min),
                   "+r" (max)
                 : "r" (key), "g" (arr [middle]),
                   "g" (middle + 1), "g" (middle));

            // Equivalent to 
            // if (key > arr [middle])
            //    min = middle + 1;
            // else
            //    max = middle;
    }
    return min;
}

我想将此 GCC 汇编器转换为与 Microsoft Visual Studio 兼容的汇编器,但作为 GCC 新手不知道从哪里开始。

任何人都可以提供帮助,或者至少解释一下 GCC 内联汇编器吗?蒂亚!

【问题讨论】:

  • 简短回答:不要这样做。将它保存在 C 中,让编译器处理它。
  • 你用 msvc 2015 u3 试过了吗?在您的情况下,它应该为该代码生成 cmovs(通过新的优化器)
  • 更快的序列可能是cmpl min,max; adc $0,middle。不确定它是否正确,可能您需要改为cmpl max,min
  • Update 3 目前是候选发布版(截至 6 月 7 日),但可以下载。 visualstudio.com/downloads/visual-studio-prerelease-downloads
  • @RossRidge 是的,我已经分析过了,这是一个关键瓶颈。是的,它的排序值。是的。我已经对其进行了分析。通过从 C# 泛型迁移到 C++ 模板和 P/Invoke,我已经获得了 100 倍的加速。额外的2折会很好;)

标签: c++ visual-studio gcc visual-c++ assembly


【解决方案1】:

重构代码以便更好地向编译器表达意图:

int binary_cmov (const int *arr, int n, int key) 
{
    int min = 0, max = n;
    while (min < max) 
    {
      int middle = (min + max) >> 1;
      min = key > arr[middle] ? middle + 1 : min;
      max = key > arr[middle] ? max : middle;
    }
    return min;
}

使用 gcc5.3 -O3,产量:

binary_cmov(int const*, int, int):
        xorl    %eax, %eax
        testl   %esi, %esi
        jle     .L4
.L3:
        leal    (%rax,%rsi), %ecx
        sarl    %ecx
        movslq  %ecx, %r8
        leal    1(%rcx), %r9d
        movl    (%rdi,%r8,4), %r8d
        cmpl    %edx, %r8d
        cmovl   %r9d, %eax
        cmovge  %ecx, %esi
        cmpl    %eax, %esi
        jg      .L3
        rep ret
.L4:
        rep ret

故事的寓意 - 不要嵌入汇编程序。您所做的只是使代码不可移植。

走得更远……

为什么不更明确地表达意图?

#include <utility>

template<class...Ts>
  auto sum(Ts...ts)
{
  std::common_type_t<Ts...> result = 0;
  using expand = int[];
  void(expand{ 0, ((result += ts), 0)... });
  return result;
}

template<class...Ts>
  auto average(Ts...ts)
{
  return sum(ts...) / sizeof...(Ts);
}

int binary_cmov (const int *arr, int n, int key) 
{
    int min = 0, max = n;
    while (min < max) 
    {
      int middle = average(min, max);
      auto greater = key > arr[middle];
      min = greater ? middle + 1 : min;
      max = greater ? max : middle;
    }
    return min;
}

编译器输出:

binary_cmov(int const*, int, int):
        xorl    %eax, %eax
        testl   %esi, %esi
        jle     .L4
.L3:
        leal    (%rax,%rsi), %ecx
        movslq  %ecx, %rcx
        shrq    %rcx
        movslq  %ecx, %r8
        leal    1(%rcx), %r9d
        movl    (%rdi,%r8,4), %r8d
        cmpl    %edx, %r8d
        cmovl   %r9d, %eax
        cmovge  %ecx, %esi
        cmpl    %eax, %esi
        jg      .L3
        rep ret
.L4:
        rep ret

【讨论】:

  • 只是一个观察。 OP 链接到 2010 年写的一篇文章,很可能当时的 GCC 编译器在这种优化方面做得不好。我已经看到大量的内联汇编代码最初是为了克服早期编译器代码生成较差的问题而编写的。通常,随着编译器开发出更好的优化技术,以后再也不会审查这些代码。
  • @MichaelPetch fair,但我认为这是避免手动优化的另一个原因。最好将糟糕的代码生成报告给编译器团队并让他们解决。在编译器中求解一次,就可以为每个编写的程序求解它——过去、现在和未来。
  • 我并不反对,因为我说这是一个观察结果(我是赞成票)。但是,如果在过去需要更快的速度并且代码生成确实输出了较差的结果,那么大多数企业可能会想要更快的代码和手动优化的汇编。当有最后期限时,大多数企业不会等待编译器赶上。企业的工作时间与编译器开发人员不同。
  • 这很有趣,因为我确实尝试过(使用三元运算符)并且没有生成 CMOV 指令。但是...@Yakk 在上面的 cmets 中指出我需要尝试 VS2015。我正在使用 VS2013。
  • @MichaelPetch “企业的工作时间与编译器开发人员不同”确实如此 :)
【解决方案2】:

对 Richard Hodges 的代码进行小幅更改还允许 Visual Studio 2013 使用 CMOV 指令:

int binary_cmov (const int *arr, int n, int key) 
{
    int min = 0, max = n;
    while (min < max) 
    {
      int middle = (min + max) >> 1;
      int middle1 = middle + 1;
      min = key > arr[middle] ? middle1 : min;
      max = key > arr[middle] ? max : middle;
    }
    return min;
}

使用cl /Ox /Fa /c t286.c 编译生成:

; Listing generated by Microsoft (R) Optimizing Compiler Version 18.00.40629.0 

binary_cmov PROC
    mov QWORD PTR [rsp+8], rbx
; Line 3
    xor eax, eax
    mov ebx, r8d
    mov r11d, edx
; Line 4
    test    edx, edx
    jle SHORT $LN9@binary_cmo
$LL2@binary_cmo:
; Line 6
    lea r10d, DWORD PTR [r11+rax]
    sar r10d, 1
; Line 8
    movsxd  r8, r10d
    lea r9d, DWORD PTR [r10+1]
    cmp ebx, DWORD PTR [rcx+r8*4]
; Line 9
    cmovg   r10d, r11d
    cmovg   eax, r9d
    mov r11d, r10d
    cmp eax, r10d
    jl  SHORT $LL2@binary_cmo
$LN9@binary_cmo:
; Line 12
    mov rbx, QWORD PTR [rsp+8]
    ret 0
binary_cmov ENDP

这也适用于 Visual Studio 2015 和 2010 编译器。使用 Visual Studio,诀窍似乎是使用三元运算符并使用简单变量作为第二个和第三个操作数。如果在上面的第一个三元运算符中将middle1 替换为middle + 1,则Visual Studio 只会为此函数生成一个CMOV 指令。第一个三元运算符会生成一个分支。

正如 Yakk 在评论中提到的,即将推出的 Visual Stdio 2015 Update 3 编译器包含对优化器的重大更新,应该会改变这种行为,使其更有可能在适当的时候生成 CMOV 指令。

【讨论】:

  • 有趣的是,您没有更容易获得cmov。我想一个分支对优化器很有吸引力,与两个cmovs 相比。对于许多编译器,使用总是发生的赋值(例如,三进制而不是 if)是发出 cmov 的主要技巧。
  • @PeterCordes 使用 GCC,等效的两个 if 语句也将最终生成 CMOV 指令。微软的编译器似乎需要三元运算符,但看起来 Visual Studio 2015 Update 3 将通过其新的基于 SSA 的优化器来改变这一点。
【解决方案3】:

人们已经(反复)指出,如果可能,您应该避免使用内联汇编。我同意所有这些。

也就是说,您还问“至少解释一下 GCC 内联汇编程序”。那部分可能没有实际意义,但 FWIW:

从汇编模板开始:

"cmpl %3, %2\n\tcmovg %4, %0\n\tcmovle %5, %1"

这里发生的一些事情对于 VS 程序员来说可能看起来很奇怪。

  1. cmpl - 默认情况下,gcc 的 asm 使用 at&t 语法而不是 intel 的。这导致许多事情发生了微妙的不同。例如,这里的l 表示比较将应用于longs(这里的long 表示4 个字节)。另一个区别之一是操作数是相反的。所以 intel 可能会使用mov eax, 1,但 att 会使用movl $1, %eax(美元符号表示常量,% 表示寄存器)。有些网站在这里讨论了所有差异。
  2. \n\t - gcc 将把代码输出到汇编器。要将这 3 条指令中的每一条放在各自的行中,您可以添加 '\n' 换行符和 '\t' 制表符以使内容整齐排列。
  3. %0-%5 - 您应该将汇编程序模板(包含汇编程序的字符串)视为您发送给 printf 的格式字符串。某些部分是按字面意思打印的,而其他部分则被替换(想想:printf("2 + 2 = %d\n", 2+2);)。同样,gcc 会用以下参数替换模板的部分内容。 %0 是第一个参数,%5 是第 6 个参数(按它们出现的顺序编号)。

这将我们带到命令的其余部分。第一个冒号之后的项目是输出参数:

             : "+r" (min),
               "+r" (max)

这里的“+”表示变量既可读取又可写入。如果它们只是输出,你会使用'='。如果您要阅读但不修改,则将其作为输入参数(即在下一个冒号之后)。 'r' 表示在执行 asm 之前必须将值移动到寄存器中。它将关于 which 寄存器的决定留给编译器。程序员不需要知道;他可以将 %0 用于min,将 %1 用于max,令牌将被适当地替换。

这将我们带到输出参数。除了 'g' 是一般约束类型之外,几乎是您所期望的。从理论上讲,它允许将值存储在寄存器、内存或立即值中。大多数情况下,它只是意味着注册。

             : "r" (key), "g" (arr [middle]),
               "g" (middle + 1), "g" (middle));

有几页关于 gcc 的内联 asm 的文档(参见 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html)详细描述了这一切。还有一些页面讨论了除 'r' 和 'g' 之外的约束 (https://gcc.gnu.org/onlinedocs/gcc/Constraints.html)。

正如您可能想象的那样,很容易出错。如果你这样做了,你可能不会得到一个很好的、可理解的编译器错误。取而代之的是,它似乎可以工作,然后十几行之后的其他东西会无缘无故地失败。

这就是为什么朋友不告诉朋友使用inline assembler

嘿,你问...

【讨论】:

  • 我正在写一个答案,确切地解释特定的 GNU C 实现如何是次优的。 (例如,64 位模式下使用 32 位变量和 64 位指针的额外符号扩展指令,key 不能变成立即数;它必须浪费一个寄存器)。当然,如果middle 是内联和展开后的编译时常量,"g" 可能会导致构建失败:cmov 不能有直接来源。无论如何,如果您打算添加其中任何一个,请不要打扰,因为我计划很快就会这样做。
  • @PeterCordes - 不打算这样做。我确实考虑过讨论符号名称和“cc”clobber,但我认为现在这个可怜的家伙已经(方式)拥有比他认为存在的更多的内联信息。似乎“不要这样做”正在成为内联 asm 标签的流行答案。
  • A "cc" clobber 对于 x86 和 x86-64 是隐式的;无需通过提出来增加混乱。我想当我发布我的答案时,主要的一点是“看看这有多难做对?我花了很长时间,而且我相当擅长”。 :P
  • 虽然“cc”在 i386 上确实是隐含的,但它并没有这样记录(尽管我努力了)。因此,它是一个实现细节,不受支持的行为,因此(理论上)可能会发生变化。不太可能(真的不太可能),但可能。是的,“这有多难”也是我的观点。真正的杀手是你永远不能绝对确定你做对了。其他地方的一些小且(看似)无关的变化可能会在数年后引发问题。
  • @DavidWohlferd 谢谢! :) 我意识到它非常容易出错。如果我可以用三元运算符实现同样的效果,那我很高兴。
【解决方案4】:

使用 VS2015 更新 3,我可以通过将第二个比较从 > 更改为 来说服编译器使用 cmova 和 cmovbe

int binary_cmov(const int *arr, int n, int key) 
{
    int min = 0, max = n;
    while (min < max) 
    // cmp         r9,r8  
    // jb          binary_cmov+1B0h 
    {
        int middle = (min + max) >> 1;
        // lea         rdx,[r8+r9]  
        // shr         rdx,1  
        int middle1 = middle + 1;
        // lea         rax,[rdx+4]  
        min = key > arr[middle] ? middle1 : min;
        // cmp         r10,dword ptr [rdi+rdx*4]  
        // cmova       r9,rax  
        max = key <= arr[middle] ? middle : max;
        // cmovbe      r8,rdx  
    }
    return min;
}

使用实际应用程序(查找断点地址)测量 i7-5600U 的性能,线性搜索对最多 512 个条目的数组执行二进制搜索。我认为速度受到填充缓存的限制,并且二进制搜索> 512条目主要表现更好,因为它避免了从内存中获取表的一部分。

使用 sentinel 进行线性指针搜索的代码是:

// the array must be terminated with a value that is larger than 
// any value searched for e.g. MAX_INT (the sentinel)
// the index of the equal or greater entry is returned
int find_in_terminated_array(const int *arr, int key) 
{
    int * p = arr;
    while(key > *p) +p;
    return p - arr;
 }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-10-07
    • 1970-01-01
    • 2019-05-11
    • 2012-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多