我的输入是2complexfloat vectors。这两个vectors都是notinterleaved:
VecAReal = Are0, Are1, Are2,...Are[N-1]
VecAImag = Aim0, Aim1, Aim2,...Aim[N-1]
VecBReal = Bre0, Bre1, Bre2,...Bre[N-1]
VecBImag = Bim0, Bim1, Bim2,...Bim[N-1]
我必须运行标量乘法:
VecCReal = Cre0, Cre1, Cre2,...Cre[N-1]
VecCImag = Cim0, Cim1, Cim2,...Cim[N-1]
Cre[i] = Are[i]*Bre[i] - Aim[i]-Bim[i]
Cim[i] = Are[i]*Bim[i] + Aim[i]+Bre[i]
在每次迭代中,我必须在4个不同的指针上运行_mm_load_ps
。
在求和、相加之后,在每次迭代中,我必须在2个不同的指针上运行_mm_store_ps
。
因为有很多的加载/存储,所以看起来效率很低。你能建议一个更好的方法吗?
1条答案
按热度按时间q9yhzks01#
不可避免的是,你必须加载所有的数据并存储它,除非你可以在一次传递中对数据做更多的工作。例如,无论你接下来要对每个
Cre
/Cim
元素做什么,当你仍然在__m128
本地变量中时,用乘法的结果向量来做。就计算强度而言,现在(如果你使用正确的公式
- Aim * Bim
而不是-Aim - Bim
,Cim的结果也是如此),每4次加载和2次存储,你要做4次乘法和2次加法/减法,或者2次乘法和2次FMA。所以这不是很好,但这种数据布局是最佳的。如果你使用一个不太方便的布局(比如交错的真实的和复数部分),你会在每个结果向量上有更多的ALU工作,但这不会是有用的工作;它是由一个不方便的存储格式创建的 Shuffle 开销。
一个
__m128
的交错数据向量只能容纳两个复数,所以每次加载操作都会加载相同数量的复数:8个复数4次加载(A和B各4次),而4个复数2次加载(A和B各2次)。因此,关键的观察点可能是每次加载/存储计算多少结果,而不仅仅是每次循环迭代的加载/存储。如果你不能将一个较早或较晚的传递合并到这项工作中,你可以尝试cache-block,这样你就可以在L1 d或L2缓存中命中,例如在一个
i
值范围内执行多个计算步骤。How does cache blocking actually speed up performance?**Footnote 1:**计算强度是每次将数据加载到寄存器时ALU的工作量。或者每次将数据带入L1 d缓存时,如果您正在测量缓存阻塞的有效性,即使您最终在一小部分数据中多次加载/存储数据。
在一个大数组上一次只做一件事对性能没有好处,即使它是一个复杂的乘法而不是一个简单的点积。现代CPU的FP数学吞吐量比DRAM或L3带宽要大得多。
但是Intel从Haswell开始可以在每个时钟进行2次加载和1次存储,或者Ice Lake可以在每个时钟进行2次加载+ 2次存储。桤木Lake可以在每个时钟进行3次加载+ 2次存储。假设它们都在L1 d缓存中命中。这适用于任何标量或向量,只要它不跨越缓存线边界,就可以达到支持的最大向量宽度。
AMD自Zen 3起每个时钟也可以执行3次加载和2次存储,但对于向量只能执行2次加载和2次存储。它每个时钟还具有2x 256位FMA(与Intel不同,它还可以在FMA/穆尔操作的同时执行2x 256位FP加法)。https://uops.info/
因此,最新一代的CPU可以非常快速地访问L1 d以保持其向量ALU的馈送,但L2缓存甚至无法跟上每个时钟周期的1个缓存线。但即使在L1 d中命中,如果适当优化(使用多个向量展开以隐藏FMA延迟),即使从L1 d开始,像点积这样的东西仍然会对负载带宽造成瓶颈,而不是FMA吞吐量。
缓存位置和流数
也许您担心涉及到多少不同的指针?
现代x86 CPU通常具有至少8路关联的L1 d缓存,因此即使在所有数组相对于4k边界(即页面大小和通常的L1 d别名步幅)处于相同偏移量的最坏情况下。Skylake客户端L2仅为4路关联,因此不幸的别名可能导致一些冲突额外未命中。
一个循环中的4个读和2个写流通常是可以的,包括L2预取器。(例如,Intel的L2预取器支持每4k页1个正向和1个反向流,IIRC。因此,如果数组太小,多个数组中的相同索引位于同一4k页内,则可能会有缺点。)
有关多个加载/存储流的详细信息,请参阅 * For-loop efficiency: merging loops *。(加载和存储竞争相同的LFB。例如Skylake有12个,因此它可以跟踪12个未完成的缓存行,这些缓存行往返于L2和更远的地方。“超级队列”跟踪来自L2的离核请求有更多的条目。)
如果你担心空间局部性,你可以用16个实数浮点数和16个虚数浮点数交替组合你的数据,例如
struct {float real[16], imag[16]};
,以防你将来想使用__m512
向量。或者,如果你可以在将来轻松地更改布局,现在使用8个浮点数的组,以便8个复数适合 * 一个 * 64字节的缓存行(如果你对齐结构体),特别是如果你曾经做过任何随机访问。
如果您可以使用结果覆盖
A
或B
之一,而不是使用单独的C
,这将保存一些内存带宽,并减少TLB页的数量。(存储到冷内存将导致加载(RFO = Read For Ownership)以及最终驱逐脏数据。)这也意味着少了一个需要跟踪的指针(对于x86-64来说不是大问题;它有15个通用的整数寄存器,与向量寄存器分开,所以对于整数循环控制来说已经足够了。)