Paw*_*ski 5 c++ x86 assembly cpu-architecture
我最近在阅读关于管道优化的文章.我想询问我是否正确理解处理器如何处理流水线操作.
这是简单测试程序的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;
}
Run Code Online (Sandbox Code Playgroud)
和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)
}
...
Run Code Online (Sandbox Code Playgroud)
在程序中,向量" vec "分配有恒定大小并用零填充.重要的"工作"发生在for循环中,其中所有向量变量都分配给5(只是一个随机值).
我想问一下这个汇编程序代码是否会在管道中出现停顿?原因是所有指令都以某种方式相关并在相同的寄存器上工作.例如,cmp rax r9在mov eax, edx实际为eax/rax赋值之前,管道需要等待指令吗?
循环10000次是分支预测应该开始工作的地方.jb指令跳转10000次,最后才会通过.这意味着分支预测器应该很容易预测跳跃将在大多数时间发生.但是,如果代码本身在循环内部停顿,那么从我的角度来看,这种优化是没有意义的.
我的目标架构是Skylake i5-6400
TL; DR:
案例1:适合L1D的缓冲区.向量构造函数或调用std::fill将把缓冲区完全放在L1D中.在这种情况下,管道和L1D高速缓存的每个周期吞吐量的1个存储是瓶颈.
情况2:适合L2的缓冲区.向量构造函数或调用std::fill将把缓冲区完全放在L2中.但是,L1必须将脏线写回L2,并且L1D和L2之间只有一个端口.另外,必须从L2到L1D取出线.L1D和L2之间的64B /周期带宽应该能够轻松处理,偶尔会有争用(详见下文).总体而言,瓶颈与案例1相同.您使用的特定缓冲区大小约为40KB,不适合英特尔和最近的AMD处理器的L1D,但适合L2.虽然在同步多线程(SMT)的情况下,其他逻辑核心可能会有一些额外的争用.
情况3:不适合L2的缓冲区.需要从L3或内存中获取这些行.L2 DPL预取器可以跟踪存储并将缓冲区预取到L2中,从而减轻长延迟.单个L2端口是L1写回和填充缓冲区的瓶颈.这是非常严重的,特别是当缓冲区不适合L3时,互连也可以在关键路径上.1个存储吞吐量对于缓存子系统来说太多了.两个最相关的性能计数器是L1D_PEND_MISS.REQUEST_FB_FULL和RESOURCE_STALLS.SB.
首先,请注意构造函数(可能会内联)vector本身通过memset内部调用将元素初始化为零.memset基本上与你的循环做同样的事情,但它是高度优化的.换句话说,就big-O表示法而言,两者都是元素数量的线性,但memset具有较小的常数因子.此外,std::fill还在内部调用memset将所有元素设置为零(再次).std::fill也可能会内联(启用适当的优化).因此,您在该段代码中确实有三个循环.使用初始化矢量会更有效std::vector<int> vec(10000u, 5).现在让我们来看看循环的微体系结构分析.我只讨论我期望在现代英特尔处理器上发生的事情,特别是Haswell和Skylake 1.
让我们仔细检查一下代码:
00007FF6A4521080 inc edx
00007FF6A4521082 mov dword ptr [rcx+rax*4],5
00007FF6A4521089 mov eax,edx
00007FF6A452108B cmp rax,r9
00007FF6A452108E jb main+80h (07FF6A4521080h)
Run Code Online (Sandbox Code Playgroud)
第一条指令将被解码为单个uop.第二条指令将被解码为两个在前端融合的微指令.第三条指令是寄存器到寄存器的移动,是寄存器重命名阶段的移动消除的候选.如果不运行代码3,很难确定移动是否会被消除.但即使没有消除,指令也将按以下方式发送2:
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)
Run Code Online (Sandbox Code Playgroud)
在cmp和jb指令将得到macrofused成一个单一的微指令.因此,融合域中的uop总数为4,未融合域中为5.他们之间只有一个跳跃.因此,每个循环可以发出单循环迭代.
由于inc和mov-store 之间存在依赖关系,因此无法在同一个循环中调度这两个指令.尽管如此,inc可以使用上一次迭代中的uops调度上一次迭代.
有四个端口(p0,p1,p5,p6)可以分配inc和mov调度.对于预测采用,只有一个端口p6 cmp/jb.STA uop有三个端口(p2,p3,p7),mov dword ptr [rcx+rax*4],5STD uop有一个端口p4.(尽管p7无法处理指定的寻址模式.)因为每个端口只有一个端口,所以可以实现的最大执行吞吐量是每个周期1次迭代.
不幸的是,吞吐量会更糟; 很多商店都会错过L1D.L1D预取程序无法在Exclusive coherence状态中预取行,也不能跟踪存储请求.但幸运的是,许多商店将合并.循环中的连续存储目标是虚拟地址空间中的顺序位置.由于一行大小为64字节,每个存储大小为4字节,因此每16个连续存储位于同一个高速缓存行.这些商店可以组合在商店缓冲区中,但它们不会,因为商店一旦成为ROB的顶端就会尽早退休.循环体非常小,因此16个商店中的少数几个不太可能在商店缓冲区中组合在一起.但是,当组合的存储请求被发送到L1D时,它将丢失并且将分配LFB,这也支持组合存储.L2缓存DPL预取器能够跟踪RFO请求,因此希望我们几乎总是能够访问L2.但是需要至少10-15个周期才能从L2到L1处获得线路.然而,在商店实际提交之前,可能会提前发送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个指令.这只有在我们几乎总能及时打入L2的情况下才有可能.缓存或内存中任何一致的延迟都会大大降低低于此的吞吐量.它可能会是这样的:一次16次迭代将快速执行,然后管道在LFB上停顿一段时间.如果该数量等于L3等待时间(约48个周期),那么吞吐量将是每3个周期(= 16/48)约1次迭代.
L1D具有有限数量(6?)的写回缓冲器来保存被驱逐的线路.此外,L2只有一个64字节端口,用于L1D和L2之间的所有通信,包括回写和RFO.回写缓冲区的可用性也可能在关键路径上.在这种情况下,LFB的数量也是一个瓶颈,因为在写回缓冲区可用之前,LFB不会被写入高速缓存.如果没有,LFB将很快填满,特别是如果L2 DPL预取器能够及时交付线路.显然,将可缓存的WB存储流传输到L1D是非常低效的.
如果您确实运行了代码,则还需要考虑两次调用memset.
(1)在Sandy Bridge和Ivy Bridge上,指令mov dword ptr [rcx+rax*4],5将被解析,导致融合域中每次迭代5次uop.所以前端可能在关键路径上.
(2)或类似的东西,取决于循环的第一次迭代的第一条指令是否获得分配器的第一个槽.如果不是,则显示的迭代次数需要相应地移位.
(3)@ PeterCordes发现移动消除确实发生在Skylake的大部分时间.我也可以在Haswell上证实这一点.