为什么"inc dword [esp + ebx]"比"inc [esp]"更快?

Run*_*oro 11 optimization x86 assembly

我有以下NASM组装程序,运行时间约为9.5秒:

section .text
global _start

_start:
  mov eax, 0
  mov ebx, 8
  loop:
    inc dword [esp + ebx]
    inc eax
    cmp eax, 0xFFFFFFFF
    jne loop

  mov eax, 1
  mov ebx, 0
  int 0x80
Run Code Online (Sandbox Code Playgroud)

但是,如果我替换[esp + ebx][esp + 8](自ebx = 8以来相同的内存位置)或甚至只是[esp],它在10.1秒内运行...

这怎么可能?是不是[esp]为CPU比计算更容易[esp + ebx]

Joh*_*ica 1

你没有对齐你的循环。
如果所有跳转指令与循环的其余部分不在同一高速缓存行中,则会产生额外的周期来获取下一个高速缓存行。

您列出的各种替代方案组装成以下编码。

0:  ff 04 1c                inc    DWORD PTR [esp+ebx*1]
3:  ff 04 24                inc    DWORD PTR [esp]
6:  ff 44 24 08             inc    DWORD PTR [esp+0x8] 
Run Code Online (Sandbox Code Playgroud)

[esp]两者[esp+reg]都以 3 个字节编码,[esp+8]占用 4 个字节。因为循环在某个随机位置开始,所以额外的字节将指令(部分)推jne loop送到下一个缓存行。

高速缓存行通常为 16 字节。

您可以通过重写代码来解决这个问题,如下所示:

  mov eax, 0
  mov ebx, 8
  .align 16             ;align on a cache line.
  loop:
    inc dword ptr [esp + ebx]                 ;7 cycles
    inc eax                                   ;0 latency drowned out by inc [mem]
    cmp eax, 0xFFFFFFFF                       ;0   "          "
    jne loop                                  ;0   "          "

  mov eax, 1
  mov ebx, 0
  int 0x80
Run Code Online (Sandbox Code Playgroud)

此循环每次迭代应执行 7 个周期。

忽略循环不做任何有用工作的事实,它可以进一步优化,如下所示:

  mov eax, 1      ;start counting at 1
  mov ebx, [esp+ebx]
  .align 16
  loop:         ;latency   ;comment
    lea ebx,[ebx+1]  ; 0   ;Runs in parallel with `add`
    add eax,1        ; 1   ;count until eax overflows
    mov [esp+8],ebx  ; 0   ;replace a R/W instruction with a W-only instruction   
    jnc loop         ; 1   ;runs in parallel with `mov [mem],reg`

  mov eax, 1
  xor ebx, ebx
  int 0x80
Run Code Online (Sandbox Code Playgroud)

此循环每次迭代应花费 2 个周期。

通过inc eax用 a替换add以及inc [esp]用不改变标志的指令替换 ,您可以允许 CPU 并行运行lea + movadd+jmp指令。
add 在较新的 CPU 上可以更快,因为add更改所有标志,而inc仅更改标志的子集。
这可能会导致指令上的部分寄存器停顿jxx,因为它必须等待对标志寄存器的部分写入得到解决。它mov [esp]也更快,因为你没有做一个read-modify-write循环,你只是在循环内写入内存。

通过展开循环可以获得进一步的收益,但收益会很小,因为这里的内存访问主导了运行时,而这从一开始就是一个愚蠢的循环。

总结一下:

  • 避免循环中的读-修改-写指令,尝试用单独的读、修改和写指令替换它们,或者将读/写移到循环之外。
  • 避免inc操作循环计数器,add而是使用。
  • lea当您对标志不感兴趣时​​,尝试使用添加。
  • 始终在高速缓存行上对齐小循环.align 16
  • 不要cmp在循环内使用,inc/add指令已经改变了标志。