将GCC内联汇编CMOV转换为Visual Studio汇编程序

时间:2016-06-12 20:29:58

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

在文章Linear vs. Binary Search中,存在使用CMOV指令的二进制搜索的快速实现。我想在VC ++中实现它,因为我正在开发的应用程序依赖于二进制搜索的性能。

enter image description here

该实现有一些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 noob不知道从哪里开始。

任何人都可以提供帮助,或至少解释一下GCC内联汇编程序吗? TIA!

4 个答案:

答案 0 :(得分:4)

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

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

答案 1 :(得分:4)

对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指令。

答案 2 :(得分:3)

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

那就是说,你也问过#34;至少要解释一下GCC Inline Assembler。&#34;那部分可能没有意义,但FWIW:

从汇编程序模板开始:

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

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

  1. cmpl - 默认情况下,gcc的asm使用&amp; t语法而不是英特尔。这导致许多事情略有不同。例如,此处的l表示比较将应用于long(在此上下文中,long表示4个字节)。另一个区别是操作数是相反的。因此,英特尔可能会使用mov eax, 1,但att会使用movl $1, %eax(美元符号表示常量,%表示注册表)。有些网站在这里谈论所有的差异。
  2. \n\t - gcc将把代码输出到汇编程序。要将这3条指令中的每条指令放在自己的行中,您需要添加&#39; \ n&#39;换线和&#39; \ t&#39;用于制作漂亮的标签。
  3. %0-%5 - 您应该考虑汇编程序模板(包含汇编程序的字符串),而不是您发送给printf的格式字符串。某些部分按字面打印,其他部分被替换(请注意:printf("2 + 2 = %d\n", 2+2);)。同样,gcc将使用以下参数替换部分模板。 %0是第一个参数,%5是第6个参数(按它们出现的顺序编号)。
  4. 这将我们带到命令的其余部分。第一个冒号后面的项是输出参数:

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

    &#39; +&#39;这表示变量都是读写的。如果仅输出,则使用&#39; =&#39;。如果您要阅读但不修改,则将其作为输入参数(即在下一个冒号之后)。 &#39; r&#39;表示在执行asm之前必须将值移入寄存器。它将关于哪个注册的决定留给了编译器。程序员不需要知道;他可以使用%0表示min而%1表示max,并且令牌将在适当时替换。

    这将我们带到输出参数。几乎是你所期望的,除了&#39; g&#39;是一般约束类型。理论上,它允许将值存储在寄存器,存储器或立即值中。大多数情况下,这仅仅意味着注册。

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

    有关于gcc的内联asm(请参阅https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html)的文档页面,这些文档详细描述了所有这些内容。还有一些页面讨论除了&#39; r&#39;之外的限制。并且&#39; g&#39; (https://gcc.gnu.org/onlinedocs/gcc/Constraints.html)。

    正如您可能想象的那样,这很容易出错。如果你这样做,你可能不会得到一个很好的,易于理解的编译器错误。相反会发生的事情是,它似乎会起作用,然后十几行之后的其他东西会因为没有明显的原因而失败。

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

    嘿,你问......

答案 3 :(得分:0)

使用VS2015更新3,我可以通过更改&gt;中的第二个比较来说服编译器使用cmova和cmovbe。到&lt; =:

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个条目的数组。我认为通过填充缓存和二进制搜索来限制速度&gt; 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;
 }