一个目标文件中的代码对齐正在影响另一个目标文件中的函数的性能

时间:2014-09-21 11:20:50

标签: c assembly x86 nasm avx

我熟悉数据对齐和性能,但我很擅长对齐代码。我最近开始使用NASM在x86-64汇编中进行编程,并且一直在使用代码对齐来比较性能。据我所知,NASM插入nop指令以实现代码对齐。

这是我在Ivy Bridge系统上尝试过的功能

void triad(float *x, float *y, float *z, int n, int repeat) {
    float k = 3.14159f;
    int(int r=0; r<repeat; r++) {
        for(int i=0; i<n; i++) {
            z[i] = x[i] + k*y[i];
        }
    }
}

我正在使用的组件如下。如果我没有指定对齐方式,那么与峰值相比,我的表现只有大约90%。但是,当我将循环之前的代码以及两个内部循环对齐到16个字节时,性能会跳跃到96%。很明显,这种情况下的代码对齐有所不同。

但这是最奇怪的部分。如果我将最里面的循环对齐到32个字节,那么这个函数的性能没有区别,但是,在另一个版本的函数中,在一个单独的目标文件中使用内在函数我将其性能从90%链接到95%!

我做了一个对象转储(使用objdump -d -M intel)对齐到16个字节的版本(我将结果发布到这个问题的结尾)和32个字节,它们是相同的!事实证明,在两个目标文件中,最内层循环无论如何都对齐到32个字节。但必须有一些区别。

我对每个目标文件进行了十六进制转储,目标文件中有一个字节不同。对齐到16个字节的目标文件具有0x10的字节,并且对应于32个字节的目标文件具有0x20的字节。 究竟发生了什么!为什么一个目标文件中的代码对齐会影响另一个目标文件中函数的性能?我如何知道将代码对齐的最佳值是什么?

我唯一的猜测是,当加载器重新定位代码时,32字节对齐的目标文件会使用内在函数影响另一个目标文件。您可以在Obtaining peak bandwidth on Haswell in the L1 cache: only getting 62%

找到要测试所有这些内容的代码

我正在使用的NASM代码:

global triad_avx_asm_repeat
;RDI x, RSI y, RDX z, RCX n, R8 repeat
pi: dd 3.14159
align 16
section .text
    triad_avx_asm_repeat:
    shl             rcx, 2  
    add             rdi, rcx
    add             rsi, rcx
    add             rdx, rcx
    vbroadcastss    ymm2, [rel pi]
    ;neg                rcx 

align 16
.L1:
    mov             rax, rcx
    neg             rax
align 16
.L2:
    vmulps          ymm1, ymm2, [rdi+rax]
    vaddps          ymm1, ymm1, [rsi+rax]
    vmovaps         [rdx+rax], ymm1
    add             rax, 32
    jne             .L2
    sub             r8d, 1
    jnz             .L1
    vzeroupper
    ret

来自objdump -d -M intel test16.o的结果。如果我在align 16之前的程序集中将align 32更改为.L2,则反汇编是相同的。但是,目标文件仍然相差一个字节。

test16.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <pi>:
   0:   d0 0f                   ror    BYTE PTR [rdi],1
   2:   49                      rex.WB
   3:   40 90                   rex xchg eax,eax
   5:   90                      nop
   6:   90                      nop
   7:   90                      nop
   8:   90                      nop
   9:   90                      nop
   a:   90                      nop
   b:   90                      nop
   c:   90                      nop
   d:   90                      nop
   e:   90                      nop
   f:   90                      nop

0000000000000010 <triad_avx_asm_repeat>:
  10:   48 c1 e1 02             shl    rcx,0x2
  14:   48 01 cf                add    rdi,rcx
  17:   48 01 ce                add    rsi,rcx
  1a:   48 01 ca                add    rdx,rcx
  1d:   c4 e2 7d 18 15 da ff    vbroadcastss ymm2,DWORD PTR [rip+0xffffffffffffffda]        # 0 <pi>
  24:   ff ff 
  26:   90                      nop
  27:   90                      nop
  28:   90                      nop
  29:   90                      nop
  2a:   90                      nop
  2b:   90                      nop
  2c:   90                      nop
  2d:   90                      nop
  2e:   90                      nop
  2f:   90                      nop

0000000000000030 <triad_avx_asm_repeat.L1>:
  30:   48 89 c8                mov    rax,rcx
  33:   48 f7 d8                neg    rax
  36:   90                      nop
  37:   90                      nop
  38:   90                      nop
  39:   90                      nop
  3a:   90                      nop
  3b:   90                      nop
  3c:   90                      nop
  3d:   90                      nop
  3e:   90                      nop
  3f:   90                      nop

0000000000000040 <triad_avx_asm_repeat.L2>:
  40:   c5 ec 59 0c 07          vmulps ymm1,ymm2,YMMWORD PTR [rdi+rax*1]
  45:   c5 f4 58 0c 06          vaddps ymm1,ymm1,YMMWORD PTR [rsi+rax*1]
  4a:   c5 fc 29 0c 02          vmovaps YMMWORD PTR [rdx+rax*1],ymm1
  4f:   48 83 c0 20             add    rax,0x20
  53:   75 eb                   jne    40 <triad_avx_asm_repeat.L2>
  55:   41 83 e8 01             sub    r8d,0x1
  59:   75 d5                   jne    30 <triad_avx_asm_repeat.L1>
  5b:   c5 f8 77                vzeroupper 
  5e:   c3                      ret    
  5f:   90                      nop

2 个答案:

答案 0 :(得分:4)

啊,代码对齐......

代码对齐的一些基础知识..

  • 大多数英特尔架构每个时钟都会获取16B的指令。
  • 分支预测器有一个更大的窗口,通常看起来是每个时钟的两倍。我们的想法是领先于所提出的指示。
  • 您的代码如何对齐将决定您可以在任何给定时钟(简单代码位置参数)解码和预测哪些指令。
  • 大多数现代英特尔架构在各种级别(在解码前的宏指令级别或解码后的微指令级别)缓存指令。只要您执行微/宏缓存,就可以消除代码对齐的影响。
  • 此外,大多数现代英特尔架构都有某种形式的循环流检测器,可以检测循环,再次从一些缓存中执行它们,绕过前端获取机制。
  • 有些英特尔架构对于它们可以缓存的内容以及它们可以做什么都很挑剔。通常依赖于指令/ uops / alignment / branches /等的数量。在某些情况下,对齐可能会影响缓存的内容和不缓存的内容,您可以创建填充可以防止或导致循环缓存的情况。
  • 为了使事情变得更复杂,指令的地址也被分支预测器使用。它们以多种方式使用,包括(1)作为查询分支预测缓冲区以预测分支,(2)作为键/值来维持某种形式的分支行为的全局状态以用于预测目的,(3)作为因此,在某些情况下,由于混叠或其他不良预测,对齐实际上会对分支预测产生相当大的影响。
  • 如果存在正确的条件,某些体系结构使用指令地址来确定何时预取数据,并且代码对齐可能会干扰该数据。
  • 对齐循环并不总是一件好事,具体取决于代码的布局方式(特别是如果循环中有控制流)。

说完所有这些等等,你的问题可能就是其中之一。看看不仅是对象的反汇编,还有可执行文件的重组,这一点非常重要。您希望在链接完所有内容后查看最终地址。在一个对象中进行更改可能会影响链接后另一个对象中指令的对齐/地址。

在某些情况下,几乎不可能以最大限度地提高性能的方式调整代码,原因很简单,因为许多低级别的架构行为很难控制和预测(不一定是这样)意味着总是如此)。在某些情况下,最好的办法是采用一些默认的对齐策略(比如对齐16B边界上的所有条目,外部循环相同),这样可以最大限度地减少性能因变化而变化的程度。作为一般策略,对齐函数条目是好的。只要您不在执行路径中添加nops,对齐相对较小的循环就是好的。

除此之外,我需要更多的信息/数据来确定您的确切问题,但认为其中一些可能有所帮助..祝您好运:))

答案 1 :(得分:3)

您看到的效果的混乱性质(汇编代码不会改变!)是由于部分对齐。在NASM中使用ALIGN宏时,它实际上有两个独立的效果:

  1. 添加0个或多个nop指令,以便下一条指令与指定的二次幂边界对齐。

  2. 发出隐式SECTALIGN宏调用,将 section alignment指令设置为对齐量 1

  3. 第一点是通常理解的对齐行为。它相对于输出文件中的部分对齐循环。

    然而,第二部分也需要:想象你的循环在汇编部分中与32字节边界对齐,但是然后运行时加载器将你的部分放在内存中,地址只对应于8个字节:这会使文件内对齐毫无意义。要解决此问题,大多数可执行格式允许每个部分指定alignment requirement,并且运行时加载器/链接器将确保将该部分加载到符合要求的内存地址。

    这就是隐藏的SECTALIGN宏的作用 - 它确保您的ALIGN宏有效。

    对于您的文件,ALIGN 16ALIGN 32之间的汇编代码没有区别,因为下一个16字节边界恰好也是下一个32字节边界(当然,每隔一个16字节边界是一个32字节的边界,因此大约一半的时间发生。隐式SECTALIGN调用仍然不同,,这是你在hexdump中看到的一个字节差异。 0x20是十进制32,0x10是十进制16。

    您可以使用objdump -h <binary>验证这一点。这是一个关于二进制的示例我对齐到32个字节:

    objdump -h loop-test.o
    
    loop-test.o:     file format elf64-x86-64
    
    Sections:
    Idx Name          Size      VMA               LMA               File off  Algn
      0 .text         0000d18a  0000000000000000  0000000000000000  00000180  2**5
                      CONTENTS, ALLOC, LOAD, READONLY, CODE
    

    2**5列中的Algn是32字节对齐方式。使用16字节对齐时,此更改为2**4

    现在应该清楚会发生什么 - 对齐示例中的第一个函数会更改节对齐,但不会更改程序集。将程序链接在一起时,链接器将合并各个.text部分并选择最高的对齐方式。

    在运行时,这会导致代码与32字节边界对齐 - 但这不会影响第一个函数,因为它不对齐敏感。由于链接器已将您的目标文件合并为一个部分,因此更大的对齐方式会更改该部分中每个函数(和指令)的对齐方式,包括您的其他方法,因此它会更改您的其他函数, 对齐敏感。

    1 准确地说,SECTALIGN仅在当前部分对齐小于指定数量时才更改部分对齐 - 因此最终部分对齐将与部分中最大的 SECTALIGN指令。