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

标签 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 的结果。如果我在 .L2 之前的程序集中将 align 16 更改为 align 32,则反汇编是相同的。但是,目标文件仍然相差一个字节。

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

最佳答案

啊啊啊,代码对齐...

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

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

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

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

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

关于一个目标文件中的代码对齐会影响另一个目标文件中函数的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25958649/

相关文章:

c++ - 在内联汇编中访问 C++ 类成员

linux - linux进程描述符存储在哪里,什么可以访问它?

c++ - mov bl 在汇编中做了什么

optimization - 取消的分支与常规分支有何不同?

android - 优化 NEON 装配功能

c - 在 linux 上测试 SIGINT 和 SIGHUP 时有趣的类似故障的行为

C程序无法编译

c - 'asm' 中不可能的约束

x86 - "memory ordering obeys causality"的含义?

c - MSVS 2015 将 errno 放入 stddef.h?