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

Z b*_*son 4 c x86 assembly 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];
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我为此使用的程序集如下。如果我不指定对齐方式,我的性能与峰值相比仅为 90% 左右。然而,当我将循环之前的代码以及两个内部循环对齐为 16 字节时,性能跃升至 96%。很明显,这种情况下的代码对齐会产生影响。

但这是最奇怪的部分。如果我将最里面的循环对齐到 32 字节,则该函数的性能没有任何差异,但是,在该函数的另一个版本中,在单独的对象文件中使用内部函数,我链接它的性能从 90% 跃升至 95%!

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

我对每个目标文件进行了十六进制转储,目标文件中有一个字节不同。与 16 字节对齐的目标文件有一个带有 的字节0x10,与 32 字节对齐的目标文件有一个带有 的字节0x20到底是怎么回事!为什么一个目标文件中的代码对齐会影响另一个目标文件中函数的性能?我如何知道将我的代码调整到的最佳值是多少?

我唯一的猜测是,当加载程序重新定位代码时,32 字节对齐的对象文件会使用内在函数影响其他对象文件。您可以在 Haswell 的 L1 缓存中获取峰值带宽中找到测试所有这些的代码:仅获得 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
Run Code Online (Sandbox Code Playgroud)

结果来自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
Run Code Online (Sandbox Code Playgroud)

小智 5

啊啊,代码对齐...

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

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

说了这么多,你的问题可能是其中之一。重要的是不仅要查看对象的反汇编,还要查看可执行文件的反汇编。您想查看所有链接后的最终地址是什么。在一个对象中进行更改可能会影响链接后另一对象中指令的对齐/地址。

在某些情况下,几乎不可能以最大化性能的方式调整代码,这仅仅是因为许多低级架构行为难以控制和预测(这并不一定意味着情况总是如此)。在某些情况下,最好的选择是采用一些默认的对齐策略(例如对齐 16B 边界上的所有条目,并且外部循环相同),以便最大限度地减少每次更改时的性能差异。作为一般策略,对齐函数条目是很好的。只要不在执行路径中添加 nop,对齐相对较小的循环就很好。

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