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

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

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

 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个字节,它们是相同的! 事实certificate,在两个目标文件中,最内层循环无论如何都对齐到32个字节。 但必须有一些区别。

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

我唯一的猜测是,当加载器重新定位代码时,32字节对齐的目标文件会使用内在函数影响另一个目标文件。 您可以在L1缓存中找到Haswell上的峰值带宽来找到测试所有这些的代码:仅获得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 16align 32上面的组件中的align 32 ,则反汇编是相同的。 但是,目标文件仍然相差一个字节。

 test16.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 : 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 : 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  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 : 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 : 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  55: 41 83 e8 01 sub r8d,0x1 59: 75 d5 jne 30  5b: c5 f8 77 vzeroupper 5e: c3 ret 5f: 90 nop 

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

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

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

第一点是通常理解的对齐行为。 它在输出文件的section中相对地对齐循环。

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

这就是隐藏的SECTALIGN宏的作用 – 它确保您的ALIGN宏工作。

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

您可以使用objdump -h validation这一点。 这是一个关于二进制的示例我对齐到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 

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

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

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


1确切地说,如果当前节对齐小于指定的量,则SECTALIGN仅更改节对齐 – 因此最终节对齐将与节中最大的 SECTALIGN指令相同。

啊,代码对齐……

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

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

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

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

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