缩放索引寻址模式是个好主意吗?

时间:2018-01-20 09:43:55

标签: gcc assembly clang x86-64 micro-optimization

请考虑以下代码:

!pip install -U -q PyDrive

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

# 1. Authenticate and create the PyDrive client.
auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

# 2. Load a file by ID and create local file.
downloaded = drive.CreateFile({'id':'fileid'}) # replace fileid with Id of file you want to access
downloaded.GetContentFile('export.csv') # now you can use export.csv 

complies(最大限度优化,但没有展开或矢量化)到...

GCC 7.2:

void foo(int* __restrict__ a)
{
    int i; int val = 0;
    for (i = 0; i < 100; i++) {
        val = 2 * i;
        a[i] = val;
    }
}

clang 5.0:

foo(int*):
        xor     eax, eax
.L2:
        mov     DWORD PTR [rdi], eax
        add     eax, 2
        add     rdi, 4
        cmp     eax, 200
        jne     .L2
        rep ret

海湾合作委员会与铿锵的方法有什么利弊?即一个额外的变量是单独递增的,与通过更复杂的寻址模式相乘?

注意:

  • 此问题还与this one的代码相同,但与foo(int*): # @foo(int*) xor eax, eax .LBB0_1: # =>This Inner Loop Header: Depth=1 mov dword ptr [rdi + 2*rax], eax add rax, 2 cmp rax, 200 jne .LBB0_1 ret 而不是float相关。

2 个答案:

答案 0 :(得分:4)

是的,利用x86寻址模式的强大功能来保存uop,如果索引没有释放到比指针增量花费更多的额外uop

(在许多情况下,展开和使用指针增量是一个胜利,因为在英特尔Sandybridge系列上没有分层,但是如果你没有展开或者你只是使用mov加载而不是将内存操作数折叠成ALU选择微融合,然后索引寻址模式在某些CPU上通常是收支平衡的,而在其他CPU上则是胜利。)

如果您想在此处做出最佳选择,请务必阅读并理解Micro fusion and addressing modes(并注意IACA错误,并且不会模拟Haswell以及后来保留一些uops微融合,所以你甚至不能通过对你进行静态分析来检查你的工作。)

索引寻址模式通常很便宜。在最坏的情况下,它们会为前端(on Intel SnB-family CPUs in some situations)花费一个额外的uop,和/或防止存储地址uop使用port7(它只支持基本+位移寻址模式)。有关英特尔在Haswell中添加的port7上的store-AGU的更多信息,请参阅Agner Fog's microarch pdf以及David Kanter's Haswell。 在Haswell +上,如果你需要你的循环每个时钟维持超过2个内存操作,那么避免索引存储。

除了机器代码编码中额外字节的代码大小成本之外,它们最多是免费的。 (索引寄存器需要编码中的SIB(Scale Index Base)字节。)

在英特尔Sandybridge系列CPU上,更常见的唯一损失是1个额外的负载使用延迟周期与简单的[base + 0-2047]寻址模式。

如果您要在多个指令中使用该寻址模式,通常只需要使用额外的指令来避免索引寻址模式。 (例如加载/修改/存储)。

如果您已经使用2寄存器寻址模式,则可以免费扩展索引(至少在现代CPU上)。对于lea,Agner Fog的表格列出AMD Ryzen具有2c延迟,lea具有缩放 - 索引寻址模式(或3分量)的每时钟吞吐量仅为2;否则1c延迟和0.25c吞吐量。例如lea rax, [rcx + rdx]lea rax, [rcx + 2*rdx]快,但不足以值得使用额外的指令。)由于某种原因,Ryzen也不喜欢64位模式下的32位目标。但最坏情况的LEA仍然没有坏。无论如何,主要与负载的地址模式选择无关,因为大多数CPU(除了有序Atom)在ALU上运行LEA,而不是用于实际加载/存储的AGU。

主要问题是单寄存器未缩放(因此它可以是机器码编码中的“基本”寄存器:[base + idx*scale + disp])或双寄存器。请注意,对于英特尔的微融合限制,[disp32 + idx*scale](例如索引静态数组)是索引寻址模式。

这两种功能都不是完全最优的(即使不考虑展开或矢量化),但clang看起来非常接近。

唯一可以做得更好的是通过避免使用add eax, 2cmp eax, 200的REX前缀来节省2个字节的代码大小。它将所有操作数提升为64位,因为它使用了指针,我猜测C环不需要它们包装,因此在asm中它使用64位。这毫无意义; 32位操作始终至少与64一样快,隐式零扩展是免费的。但是这只需要2个字节的代码大小,并且除了间接前端效果之外不会产生任何性能。

你已经构造了你的循环,所以编译器需要在寄存器中保留一个特定的值,并且不能完全将问题转换为指针增量+与结束指针的比较(编译器经常在它们不执行时进行比较)除了数组索引之外,还需要循环变量。

你也无法转换为将负索引计数到零(编译器从不这样做,但是将循环开销减少到Intel CPU上总共1个宏融合add + branch uop(可以融合{{1虽然AMD只能融合测试或cmp / jcc)。

Clang做得很好,注意到它可以使用add + jcc作为数组索引(以字节为单位)。这是tune = generic的一个很好的优化。索引商店将在英特尔Sandybridge和Ivybridge上取消层压,但在Haswell及更高版本上保持微观融合。 (在其他CPU上,比如Nehalem,Silvermont,Ryzen,Jaguar等等,没有任何劣势。)

gcc的循环在循环中有1个额外的uop。理论上它仍然可以在Core2 / Nehalem上以每个时钟1个存储运行,但它正好与每个时钟限制的4个uop相对应。 (实际上,Core2无法在64位模式下将cmp / jcc宏熔合,因此它在前端存在瓶颈)。

答案 1 :(得分:2)

索引寻址(在加载和存储中,lea仍然不同)有一些权衡,例如

  • 在许多μarch上,使用索引寻址的指令比不指令的指令具有稍长的延迟。但通常吞吐量是一个更重要的考虑因素。
  • 在Netburst上,SIB byte的商店会产生额外的μop,因此也可能会产生吞吐量。无论您是否将其用于索引寻址,SIB字节都会产生额外的μop,但索引寻址总是会花费额外的μop。它不适用于负载。
  • 在Haswell / Broadwell(仍然在Skylake / Kabylake),具有索引寻址的商店不能使用port 7来生成地址,而是使用一个更通用的地址生成端口,从而降低了可用于负载的吞吐量

因此对于加载,如果它在某处保存了一个add,那么使用索引寻址通常是好的(或者不坏),除非它们是依赖加载链的一部分。对于商店来说,使用索引寻址更危险。在示例代码中,它不应该产生很大的差异。保存add并不真正相关,ALU说明不会成为瓶颈。端口2或3中发生的地址生成无关紧要,因为没有负载。