【问题标题】:GNU inline assembly optimisationGNU 内联汇编优化
【发布时间】:2012-09-18 05:16:29
【问题描述】:

我正在尝试为高度优化的 x86-64 位操作代码编写一个小型库,并且正在摆弄内联 asm。

在测试这个特殊案例时引起了我的注意:

unsigned long test = 0;
unsigned long bsr;

// bit test and set 39th bit
__asm__ ("btsq\t%1, %0 " : "+rm" (test) : "rJ" (39) );

// bit scan reverse (get most significant bit id)
__asm__ ("bsrq\t%1, %0" : "=r" (bsr) : "rm" (test) );

printf("test = %lu, bsr = %d\n", test, bsr);

在 gcc 和 icc 中都可以正常编译和运行,但是当我检查程序集时会发现差异

gcc -S -fverbose-asm -std=gnu99 -O3

movq    $0, -8(%rbp)
## InlineAsm Start
btsq    $39, -8(%rbp) 
## InlineAsm End
movq    -8(%rbp), %rax
movq    %rax, -16(%rbp)
## InlineAsm Start
bsrq    -16(%rbp), %rdx
## InlineAsm End
movq    -8(%rbp), %rsi
leaq    L_.str(%rip), %rdi
xorb    %al, %al
callq   _printf

我想知道为什么这么复杂?我正在编写高性能代码,其中指令的数量至关重要。我特别想知道为什么 gcc 在将变量 test 传递给第二个内联 asm 之前会对其进行复制?

使用 icc 编译的相同代码会产生更好的结果:

    xorl      %esi, %esi                                    # test = 0
    movl      $.L_2__STRING.0, %edi                         # has something to do with printf
    orl       $32832, (%rsp)                                # part of function initiation
    xorl      %eax, %eax                                    # has something to do with printf
    ldmxcsr   (%rsp)                                        # part of function initiation
    btsq      $39, %rsi                                     #106.0
    bsrq      %rsi, %rdx                                    #109.0
    call      printf                                        #111.2

尽管 gcc 决定将我的变量保留在堆栈中而不是寄存器中,但我不明白为什么要在将 test 传递给第二个 asm 之前复制它? 如果我将test 作为第二个 asm 中的输入/输出变量放入

__asm__ ("bsrq\t%1, %0" : "=r" (bsr) , "+rm" (test) );

然后那些线消失了。

movq    $0, -8(%rbp)
## InlineAsm Start
btsq    $39, -8(%rbp) 
## InlineAsm End
## InlineAsm Start
bsrq    -8(%rbp), %rdx
## InlineAsm End
movq    -8(%rbp), %rsi
leaq    L_.str(%rip), %rdi
xorb    %al, %al
callq   _printf

这是 gcc 搞砸了优化还是我错过了一些重要的编译器开关?我的生产系统确实有 icc,但如果我决定在某个时候分发源代码,那么它也必须能够使用 gcc 进行编译。

使用的编译器:

gcc 版本 4.2.1(基于 Apple Inc. build 5658)(LLVM build 2336.1.00)

icc 版本 12.0.2

【问题讨论】:

  • 为什么不对这些操作使用标准的内在函数呢?这样编译器会为您完成大部分工作(而且它可能也会做得更好)。
  • 嗯,首先这些并不是我打算使用的唯一说明,它只是示例。 gcc 对它们中的大多数都有内在函数,幸运的是,icc 并没有那么幸运地拥有内在函数,而且我仅使用 icc 编译就有 20-30% 的性能提升。
  • 好的 - 但除非有一些特定的东西不能通过内在函数完成,否则我仍然相信如果你允许编译器会比你做得更好。
  • 除了我不是在寻找编译器内在函数(我知道在哪里可以找到它们)这一事实之外,它们会产生比我使用的指令多得多的指令,并且在不同的编译器中会产生不同的结果。 __builtin_clzll(0) 在使用 gcc 编译时为 64,在 icc 中为 63。我在这个主题中的问题是如何优化内联 asm 的嵌入。
  • 您能否提供一个完整的源示例,使 gcc 发出您显示的汇编代码?我已经尝试将您的 sn-p 嵌入到 main() 并使用从 3.2.3 到 4.7.2 的各种 gcc 版本对其进行编译,并且没有复制将 test 放入堆栈中 -O3 opt 的代码等级。请提供更多背景信息。

标签: optimization gcc x86-64 inline-assembly icc


【解决方案1】:

我已经像这样在 Linux 上尝试过你的示例(通过在 printf: 中使用 &test 来强制为 test 使用堆栈 ref/loc 使其“邪恶”:):

#include <stdio.h>
int main(int argc, char **argv)
{
    unsigned long test = 0;
    unsigned long bsr;
// bit test and set 39th bit
    asm ("btsq\t%1, %0 " : "+rm" (test) : "rJ" (39) );
// bit scan reverse (get most significant bit id)
    asm ("bsrq\t%1, %0" : "=r" (bsr) : "rm" (test) );
    printf("test = %lu, bsr = %d, &test = %p\n", test, bsr, &test);
    return 0;
}
并用各种版本的gcc -O3 编译它...得到以下结果: 代码生成的 gcc 版本 ==================================================== =============================== 400630: 48 83 ec 18 sub $0x18,%rsp 4.7.2, 400634: 31 c0 xor %eax,%eax 4.6.2, 400636: bf 50 07 40 00 移动 $0x400750,%edi 4.4.6 40063b: 48 8d 4c 24 08 lea 0x8(%rsp),%rcx 400640: 48 0f ba e8 27 bts $0x27,%rax 400645: 48 89 44 24 08 移动 %rax,0x8(%rsp) 40064a: 48 89 c6 移动 %rax,%rsi 40064d: 48 0f bd d0 bsr %rax,%rdx 400651: 31 c0 xor %eax,%eax 400653:e8 68 fe ff ff callq 4004c0 [ ... ] -------------------------------------------------- ------------------------------------------- 4004f0: 48 83 ec 18 sub $0x18,%rsp 4.1 4004f4: 31 c0 xor %eax,%eax 4004f6: bf 28 06 40 00 移动 $0x400628,%edi 4004fb: 48 8d 4c 24 10 lea 0x10(%rsp),%rcx 400500: 48 c7 44 24 10 00 00 00 00 movq $0x0,0x10(%rsp) 400509: 48 0f ba e8 27 bts $0x27,%rax 40050e: 48 89 44 24 10 移动 %rax,0x10(%rsp) 400513: 48 89 c6 移动 %rax,%rsi 400516: 48 0f bd d0 bsr %rax,%rdx 40051a: 31 c0 xor %eax,%eax 40051c:e8 c7 fe ff ff callq 4003e8 [ ... ] -------------------------------------------------- ------------------------------------------- 400500: 48 83 ec 08 sub $0x8,%rsp 3.4.5 400504: bf 30 06 40 00 移动 $0x400630,%edi 400509: 31 c0 xor %eax,%eax 40050b: 48 c7 04 24 00 00 00 00 movq $0x0,(%rsp) 400513: 48 89 e1 移动 %rsp,%rcx 400516: 48 0f ba 2c 24 27 btsq $0x27,(%rsp) 40051c: 48 8b 34 24 移动 (%rsp),%rsi 400520: 48 0f bd 1​​4 24 bsr (%rsp),%rdx 400525:e8 fe fe ff ff callq 400428 [ ... ] -------------------------------------------------- ------------------------------------------- 4004e0: 48 83 ec 08 sub $0x8,%rsp 3.2.3 4004e4: bf 10 06 40 00 移动 $0x400610,%edi 4004e9: 31 c0 xor %eax,%eax 4004eb: 48 c7 04 24 00 00 00 00 movq $0x0,(%rsp) 4004f3: 48 0f ba 2c 24 27 btsq $0x27,(%rsp) 4004f9: 48 8b 34 24 移动 (%rsp),%rsi 4004fd: 48 89 e1 移动 %rsp,%rcx 400500: 48 0f bd 1​​4 24 bsr (%rsp),%rdx 400505:e8 ee fe ff ff callq 4003f8 [ ... ]

虽然创建的代码存在显着差异(包括 bsr 是否将 test 访问为寄存器或内存),但所有经过测试的 rev 都不会重新创建您显示的程序集。我怀疑您在 MacOSX 上使用的 4.2.x 版本中存在错误,但是我没有您的测试用例,也没有可用的特定编译器版本。

编辑: 上面的代码明显不同,它强制test 进入堆栈;如果这没有完成,那么我测试过的所有“普通”gcc版本都会直接配对bts $39, %rsi/bsr %rsi, %rdx

不过,我发现 clang 在那里创建了不同的代码:

 140: 50 推送 %rax
 141: 48 c7 04 24 00 00 00 00 movq $0x0,(%rsp)
 149: 31 f6 异或 %esi,%esi
 14b: 48 0f ba ee 27 bts $0x27,%rsi
 150: 48 89 34 24 移动 %rsi,(%rsp)
 154: 48 0f bd d6 bsr %rsi,%rdx
 158: bf 00 00 00 00 移动 $0x0,%edi
 15d: 30 c0 xor %al,%al
 15f: e8 00 00 00 00 callq printf@plt>
所以区别似乎确实在 clang/llvm 和“gcc proper”的代码生成器之间。

【讨论】:

  • 是的,这似乎是一个奇怪的苹果开发套件 gcc。我还尝试用 gcc 在我们的集群上编译它,我得到的代码与 icc 生成的代码非常相似。我相信我正在寻找的优化器开关是-fforward-propagate,它甚至不是苹果开发套件 gcc 的一部分。知道我的 macbook pro 不是生产系统,我会让这个问题得到解决。
猜你喜欢
  • 1970-01-01
  • 2011-07-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多