微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

展开将如何影响每个元素计数 CPE 的周期

如何解决展开将如何影响每个元素计数 CPE 的周期

  1. 如何使用这些代码片段计算 CPE(每个元素的周期数)?
  2. 两个给定代码片段之间的 CPE 有何不同?

我有这段代码

void randomFunction(float a[],float Tb[],float c[],long int n){
        int i,j,k;
        for(i=0;i<n;++i)
            for(j=0;j<n;++j)
                for(k=0;k<n-1;k++){ 
                                temp+= a[i*n+k] * Tb[j*n+k];
                }
    
}

这是最内层循环的程序集,来自 GCC10.3 -O2 (https://godbolt.org/z/cWE16rW1r),用于返回具有本地 float temp=0;函数版本,因此循环不会被优化掉:

.L4:
    movss   (%rcx,%rax),%xmm0
    mulss   (%rdx,%xmm0
    addq    $4,%rax
    addss   %xmm0,%xmm1
    cmpq    %rax,%rsi
    jne     .L4

现在我正在尝试使用展开来“优化它”。

void randomUnrollingFunction(float a[],long int n){
    int i,k;
    for(i=0;i<n;++i)
        for(j=0;j<n;++j)
            for(k=0;k<n-1;k+=2){//this is the unrolled portion by 2
                            temp+= a[i*n+k] * Tb[j*n+k];
                            temp+= a[i*n+k+1]* Tb[j*n+k+1];
            }

}

我想知道通过因子 2 展开的估计 CPE 是多少。
CPE是周期数/指令数

这是延迟的信息:

latency info

提前感谢您的帮助!

解决方法

您的循环在 addss 延迟(浮动添加)上完全瓶颈,每 1 个元素 3 个周期。乱序 exec 让其他工作在它的“影子”中运行。假设你的类似 Haswell 的 CPU 有足够的内存带宽来跟上1

这种展开方式根本没有帮助,不会改变串行依赖链,除非你用 -ffast-math 或其他东西让编译器把它变成

temp1 += a[...+0]* Tb[...+0];
temp2 += a[...+1]* Tb[...+1];

或者在将它们提供给串行依赖之前添加对,例如

temp +=  a[]*Tb[] + a[+1]*Tb[+1];

一个长的串行依赖链是最糟糕的,而且在数值上也不是很好:成对求和(或者特别是使用多个累加器在这个方向上的一步)在数值上会更好,而且性能也更好。 (Simd matmul program gives different numerical results)。

(如果您的多个累加器是 SIMD 向量的 4 个元素,您可以使用相同模式的 asm 指令完成 4 倍的工作量。但是您需要展开多个 向量,因为addps 在现代 x86 CPU 上具有与 addss 相同的性能特征。)

脚注 1:每 3 个周期 4 个字节的两个连续读取流;当然,台式机 Haswell 可以跟上,甚至可能是 Xeon 与许多其他内核竞争内存带宽。但是从 a[] 读取的内容可能会命中缓存,因为 a[i*n+k] 是重复的同一行,直到我们继续进行下一次外循环迭代。因此,当我们扫描一行 a[] 时,只有 1 行 Tb 必须在缓存中保持热状态(以便在下一次中间迭代中获得命中)。因此,如果 a[] 不是巨大,则 n 必须从 DRAM 中进入一次,但我们按顺序遍历整个 Tb[] {{1} } 次。


更详细的版本

参见 What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand? - 查找依赖链(在本例中,naddss)。还有A whirlwind introduction to dataflow graphs

然后寻找后端和前端的吞吐量瓶颈。在您的情况下,延迟占主导地位。 (假设前端匹配这个 Haswell 后端,尽管跟上这个后端延迟瓶颈并不需要太多。另外,我讨厌他们给他们的“功能单元”编号从 1 开始,而不是遵循 Intel 对具有 ALU 的端口 0、1、5、6 进行编号。Haswell 的 FP 加法器在端口 1 上,端口 2/3 是加载/存储 AGU 单元等)

ADDSS 有 3 个周期的延迟,因此 %xmm1 每 3 个周期只能执行一次。 load / MULSS 操作只是独立地为 ADDSS 准备输入,循环开销也是独立的。


请注意,如果不是延迟瓶颈,您的循环将在真实 Haswell 的前端(每个周期 4 个融合域 uops)上出现瓶颈,而不是在后端功能单元上。循环是 5 个融合域 uops,假设 cmp/jne 的宏融合,并且尽管采用索引寻址模式,Haswell 仍可以保持内存源添加微融合。 (Sandybridge would un-laminate it。)

在一般情况下,了解后端功能单元是不够的。前端也是一个常见的瓶颈,尤其是在必须存储一些东西的循环中,比如实际的 matmul。

但是由于对 ADDSS 的串行依赖(它实际上跨越外循环),唯一重要的是依赖链。

即使是对内循环的最后一次迭代的分支预测错误(当分支未被采用而不是正常采用时),这只会给后端更多时间来咀嚼那些挂起的 ADDSS 操作,而前端自行排序并开始下一个内部循环。

由于您以不改变串行依赖的方式展开,因此除了微小的 temp += ... 之外,它对性能的影响为零。(对于微小的 n,整个事情可能会在调用之前/之后与调用者的独立工作重叠。在这种情况下,保存指令可能会有所帮助,还允许乱序执行程序“看得更远”。Understanding the impact of lfence on a loop with two long dependency chains,for increasing lengths在这种情况下,OoO exec 可以(部分)重叠两个独立的 n dep 链,这些链按照程序顺序一个接一个。

当然,到那时,您正在考虑所展示内容之外的代码。即使对于 n=10,也就是 10^3 = 1000 次内部迭代,而 Haswell 的 ROB 也只有 192 uops 大,RS 为 60 个条目。 (https://www.realworldtech.com/haswell-cpu/3/)。


以有用的方式展开

另见Why does mulss take only 3 cycles on Haswell,different from Agner's instruction tables? (Unrolling FP loops with multiple accumulators) re:以确实创建更多指令级并行性、隐藏 FP 依赖链的方式展开。

展开不同的是,每次循环迭代只求和一次到 imul 中,每次迭代将保持相同的周期,同时仍然使您处理的元素数量加倍。

temp

显然,您可以继续这样做,直到遇到前端或后端吞吐量限制,例如每个时钟增加一个。上面的版本每 3 个时钟增加两次。

您的“功能单元”表没有列出 FMA(融合乘加),但真正的 Haswell 有它,性能与 FP mul 相同。如果有的话,它不会有太大帮助,因为您当前的循环结构每个 mul+add 执行 2 个负载,因此将其减少到 2 个负载和一个 FMA 仍然会成为负载瓶颈。可能有助于提高前端吞吐量?

可能有助于减少负载的是展开外循环之一,同时使用 for(k=0;k<n-1;k+=2){//this is the unrolled portion by 2 temp += (a[i*n+k] * Tb[j*n+k] + a[i*n+k+1]* Tb[j*n+k+1]); } a[i*n+k] 以及一个 a[(i+1)*n+k]。这当然会改变计算顺序,因此对于没有 Tb[j*n+k] 的编译器来说是不合法的,因为 FP 数学不是严格关联的。


这是一个 matmul 的减少,允许更好的优化

(等等,你的代码没有显示 -ffast-math 被重新初始化的位置,或者 temp 参数的用途。我只是假设它是全局的或其他东西,但可能你真的屠杀了一个普通的 matmul 函数,它在每次内部迭代后将单独的 c[] 存储到 temp 中。在这种情况下,单独的中间循环迭代之间的 OoO exec 与中等相关-size c[]。但是您没有显示调度程序/ROB 大小,这不是您可以轻松建模的东西;您需要实际进行基准测试。所以本节可能仅适用于我发明的问题,而不适用于什么你是想问!)

您的循环似乎在对 matmul 结果的元素求和,但仍然像 matmul 一样结构化。即进行行 x 列点积,但不是将其存储到 N x N n 矩阵中,而是对结果求和。

这相当于对两个矩阵之间元素的每对乘积求和。由于我们不再需要将单独的行 x 列点积分开,这使得 lot 优化! (这个求和顺序没有什么特别或理想的;其他顺序会有不同但可能不会更糟的舍入误差。)

例如,您不需要将 b 转置为 Tb,只需使用 b(除非它自然已经转置了,在这种情况下就可以了)。您的矩阵是方阵,所以根本没有

此外,您可以简单地从 result[...] 加载一个或几个元素并循环遍历 a[],使用 FMA 操作来执行这些乘积,每个 FMA 加载一个,放入 10 或 12 个向量累加器。 (或者当然缓存阻止它以循环 Tb 的连续部分,该部分可以在 L1d 缓存中保持热度。)

这可能接近 Haswell 的最大 FLOPS 吞吐量,即每个时钟 2x 256 位 FMA = 8(每个 YMM 向量 Tb[] 个元素)x 2 FMA/时钟 x 2 FLOP/FMA = 32 FLOP/时钟。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。