gpt4 book ai didi

c++ - 寄存器流水线计算

转载 作者:行者123 更新时间:2023-12-03 18:00:28 25 4
gpt4 key购买 nike

我最近在阅读有关管道优化的文章。我想问一下我是否正确理解了处理器如何处理流水线。

这是简单测试程序的C++代码:

#include <vector>

int main()
{
std::vector<int> vec(10000u);
std::fill(vec.begin(), vec.end(), 0);
for (unsigned i = 0u; i < vec.size(); ++i)
{
vec[i] = 5;
}

return 0;
}

以及由for循环产生的部分汇编代码:
...
00007FF6A4521080 inc edx
{
vec[i] = 5;
00007FF6A4521082 mov dword ptr [rcx+rax*4],5
00007FF6A4521089 mov eax,edx
00007FF6A452108B cmp rax,r9
00007FF6A452108E jb main+80h (07FF6A4521080h)
}
...

在程序中, vector “vec”被分配为恒定大小并填充零。重要的“工作”发生在for循环中,其中所有 vector 变量都分配为5(只是一个随机值)。

我想问一下这个汇编代码是否会使管道停滞不前?原因是所有指令都以某种方式相关并且在同一寄存器上工作。例如,在 cmp rax r9实际将值分配给eax/rax之前,管道需要等待 mov eax, edx指令。

循环10000次是分支预测应该起作用的地方。 jb指令跳转10000次,只有最后它会通过。这意味着分支预测器应该非常容易地预测大多数情况下会发生跳跃。但是,从我的角度来看,如果代码本身在循环内停滞,那么这种优化将毫无意义。

我的目标架构是Skylake i5-6400

最佳答案

TL; DR:

情况1:适合L1D的缓冲区。 vector 构造函数或对std::fill的调用会将缓冲区完全放在L1D中。在这种情况下,流水线和L1D缓存的每周期1个存储吞吐量是瓶颈。

情况2:适合L2的缓冲区。 vector 构造函数或对std::fill的调用会将缓冲区完全放置在L2中。但是,L1必须将脏线写回到L2,并且在L1D和L2之间只有一个端口。另外,必须从L2到L1D提取线。 L1D和L2之间的64B/周期带宽应该能够轻松解决这一问题,也许偶尔会有争用(有关更多详细信息,请参见下文)。因此,总体瓶颈与案例1相同。您使用的特定缓冲区大小(大约40KB)不适用于Intel的L1D和最近的AMD处理器,但适合L2。尽管在同时多线程(SMT)的情况下,其他逻辑核心可能会有一些其他争用。

情况3:L2中不适合的缓冲区。这些行需要从L3或内存中获取。 L2 DPL预取器可以跟踪存储并将缓冲区预取到L2中,从而减轻了长等待时间。单个L2端口是L1写回和填充缓冲区的瓶颈。这很严重,特别是当缓冲区不适合L3且互连也可能位于关键路径上时。 1存储吞吐量对于缓存子系统来说太高了。两个最相关的性能计数器是L1D_PEND_MISS.REQUEST_FB_FULL和和RESOURCE_STALLS.SB

首先,请注意vector本身的构造函数(可能会内联)通过内部调用memset将元素初始化为零。 memset基本上与循环执行相同的操作,但已高度优化。换句话说,就大O表示而言,两者的元素数量都是线性的,但是memset的常数因子较小。此外,std::fill还内部调用memset将所有元素设置为零(再次)。 std::fill也可能会内联(启用适当的优化)。因此,您在那段代码中确实有三个循环。使用std::vector<int> vec(10000u, 5)初始化 vector 将更加有效。现在让我们进入循环的微体系结构分析。我只会讨论我期望在现代Intel处理器上发生的事情,特别是Haswell和Skylake1。

让我们仔细检查代码:

00007FF6A4521080  inc         edx
00007FF6A4521082 mov dword ptr [rcx+rax*4],5
00007FF6A4521089 mov eax,edx
00007FF6A452108B cmp rax,r9
00007FF6A452108E jb main+80h (07FF6A4521080h)

第一条指令将被解码为单个uop。第二条指令将被解码为在前端融合的两个微指令。第三条指令是寄存器到寄存器的移动,并且是在寄存器重命名阶段消除移动的候选。很难确定如果不运行code3是否会消除这一步。但是,即使没有消除它,也会按照以下方式分派(dispatch)指令:
               dispatch cycle                            |         allocate cycle

cmp rax,r9 macro-fused | inc edx (iteration J+3)
jb main+80h (07FF6A4521080h) (iteration J) | mov dword ptr [rcx+rax*4],5 (iteration J+3)
mov dword ptr [rcx+rax*4],5 (iteration J+1)| mov eax,edx (iteration J+3)
mov eax,edx (iteration J+1)| cmp rax,r9 macro-fused
inc edx (iteration J+2)| jb main+80h (07FF6A4521080h) (iteration J+3)
---------------------------------------------------------|---------------------------------------------------------
cmp rax,r9 macro-fused | inc edx (iteration J+4)
jb main+80h (07FF6A4521080h) (iteration J+1)| mov dword ptr [rcx+rax*4],5 (iteration J+4)
mov dword ptr [rcx+rax*4],5 (iteration J+2)| mov eax,edx (iteration J+4)
mov eax,edx (iteration J+2)| cmp rax,r9 macro-fused
inc edx (iteration J+3)| jb main+80h (07FF6A4521080h) (iteration J+4)
cmpjb指令将被宏融合到单个uop中。因此,在融合域中,微指令的总数为4,在非融合域中为5。他们之间只有一跳。因此,每个循环可以发出一个单循环迭代。

由于 incmov -store之间的依赖性,因此无法在同一周期内分派(dispatch)这两条指令。但是,可以使用来自先前迭代的uops调度来自先前迭代的 inc

可以将 incmov分配到四个端口(p0,p1,p5,p6)。预测采用的 cmp/jb恰好有一个端口p6。 mov dword ptr [rcx+rax*4],5的STA uop有三个端口(p2,p3,p7),而STD uop有一个端口p4。 (尽管p7无法处理指定的寻址模式。)由于每个端口只有一个端口,因此可以实现的最大执行吞吐量是每个周期1次迭代。

不幸的是,吞吐量会更糟。许多商店都会错过L1D。 L1D预取器无法以独占一致性状态预取行,并且无法跟踪存储请求。但幸运的是,许多商店将合并。循环中的连续存储目标是虚拟地址空间中的顺序位置。由于一行的大小为64个字节,每个存储区的大小为4个字节,因此,每16个连续的存储区都位于同一缓存行中。这些存储可以合并到存储缓冲区中,但是它们不会,因为一旦它们成为ROB的顶部,它们将尽早退休。循环体很小,因此在存储缓冲区中合并的16个存储中很少有几个是不太可能的。但是,当合并的存储请求发送到L1D时,它将丢失并分配LFB,这也支持合并存储。 L2缓存DPL预取器能够跟踪RFO请求,因此希望我们几乎总是会遇到L2。但是从L2到L1的线路至少需要10-15个周期。不过,RFO可能会在商店实际提交之前提前发送。同时,很可能需要从L1清除脏线,以从要写入的输入线中腾出空间。被逐出的行将被写在回写缓冲区中。

如果不运行代码,很难预测整体效果。两个最相关的性能计数器是 L1D_PEND_MISS.REQUEST_FB_FULL和和 RESOURCE_STALLS.SB

L1D在Ivy Bridge,Haswell和Skylake上只有一个分别为16字节,32字节,64字节的存储端口。因此,商店将致力于这些粒度。但是,单个LFB始终可以容纳完整的64字节缓存行。

商店融合的微码总数等于元素数(在这种情况下为100万)。要获得所需的LFB数量,请除以16得到62500 LFB,这与到L2的RFO数量相同。需要另一个LFB之前将需要16个周期,因为每个周期只能调度一个存储。只要L2可以在16个周期内交付目标行,我们就永远不会阻塞LFB,并且实现的吞吐量将接近每个周期1次迭代,或者就IPC而言,每个周期5条指令。这只有在我们几乎总是及时将int L2击中时才有可能。高速缓存或内存中任何持续的延迟都将大大降低吞吐量。它可能是这样的:16次迭代的突发将快速执行,然后管道在LFB上停顿一定数量的周期。如果此数字等于L3延迟(大约48个周期),那么吞吐量将约为每3个周期1次迭代(= 16/48)。

L1D具有有限数量(6?)的回写缓冲区以容纳逐出的行。此外,L2仅具有一个64字节的端口,用于L1D和L2之间的所有通信,包括回写和RFO。回写缓冲区的可用性也可能位于关键路径上。在那种情况下,LFB的数量也成为瓶颈,因为只有在有写回缓冲区可用之前,LFB才会被写入高速缓存。否则,LFB将 swift 填满,特别是如果L2 DPL预取器能够及时交付线路。显然,将可缓存的WB存储流式传输到L1D是非常低效的。

如果您确实运行了代码,则还需要考虑对 memset的两次调用。

(1)在Sandy Bridge和Ivy Bridge上, the instruction mov dword ptr [rcx+rax*4],5 will get unlaminiated,在融合域中每次迭代产生5 oups。因此,前端可能位于关键路径上。

(2)或类似的东西,取决于循环的第一次迭代的第一条指令是否获得分配器的第一个插槽。如果不是,则需要相应地移动所示的迭代数。

(3)@PeterCordes发现,在大多数情况下,在Skylake上都确实存在消除移动的情况。我也可以在Haswell上确认这一点。

关于c++ - 寄存器流水线计算,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52280958/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com