Zvi*_*red 1 c x86 sse intrinsics complex-numbers
我的输入是 2 个复数浮点向量。两个向量不交错:
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]
Run Code Online (Sandbox Code Playgroud)
我必须运行标量乘法:
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]
Run Code Online (Sandbox Code Playgroud)
在每次迭代中,我必须_mm_load_ps
在 4 个不同的指针上运行。
在 sum、add 之后,在每次迭代中我必须_mm_store_ps
在 2 个不同的指针上运行。
由于大量加载/存储,它看起来效率很低。你能建议一个更好的方法吗?
您不可避免地必须加载所有数据并存储它,除非您可以一次对数据进行更多工作。Cre
例如,无论您接下来要对每个/元素执行什么操作Cim
,都可以使用乘法结果向量来执行此操作,同时将它们保留在__m128
本地变量中。
就计算强度1而言,现在(如果您使用正确的公式代替- Aim * Bim
,-Aim - Bim
并且与 Cim 结果类似),您将每 4 次加载和 2 次存储进行 4 次乘法和两次加/减。或者两个乘法和两个 FMA。所以这不是很好,但这种数据布局是最佳的。
如果您使用不太方便的布局(例如交错的实部和复数部分),则每个结果向量都会有更多的 ALU 工作,但这不会是有用的工作;这是由不方便的存储格式造成的洗牌开销。
一个__m128
交错数据向量只能保存两个复数。因此,每次加载操作都会加载相同数量的复数:8 个复数加载 4 次(A 和 B 各 4 个),而 4 个复数加载 2 次(A 和 B 各 2 个)。因此,也许关键的观察是每次加载/存储要计算多少结果,而不仅仅是每次循环迭代的加载/存储。
如果您无法将较早或较晚的传递折叠到这项工作中,您可以尝试缓存块,以便命中 L1d 或 L2 缓存,例如对一系列值进行多个计算步骤i
。 缓存阻塞实际上如何提高性能?
脚注 1: 计算强度是每次将数据加载到寄存器时 ALU 的工作量。或者,每次将其放入 L1d 缓存时,如果您正在测量缓存阻塞的有效性,即使您最终在处理数据的一小部分区域中多次加载/存储它。
在一个大数组上一次只做一件事对性能不利,即使它是复杂的乘法而不是简单的点积。现代 CPU 的 FP 数学吞吐量比 DRAM 或 L3 带宽高得多。
但 Intel 自 Haswell 以来可以在每个时钟进行 2 次加载和 1 次存储,或者 Ice Lake 每个时钟进行 2 次加载 + 2 次存储。Alder Lake 每个时钟 3 次加载 + 2 次存储。假设它们都命中 L1d 缓存。这适用于最大支持的最大向量宽度的任何标量或向量,只要它不跨越缓存行边界即可。
自 Zen 3 起,AMD 也可以在每个时钟执行 3 次加载和 2 次存储,但对于向量只能执行 2 次加载 + 2 次存储。每个时钟还具有 2x 256 位 FMA(与 Intel 不同,它还可以在 FMA/mul 操作的同时进行 2x 256 位 FP 加法。) https://uops.info/
因此,最新一代的 CPU 可以非常快速地访问 L1d,以保持矢量 ALU 的运行,但 L2 缓存甚至无法跟上每个时钟周期 1 个缓存行的速度。但即使达到 L1d,如果适当优化(使用多个向量展开以隐藏 FMA 延迟),点积之类的东西仍然是 L1d 负载带宽的瓶颈,而不是 FMA 吞吐量的瓶颈。特别是对于实数,而不是复杂的。
也许您担心涉及多少个不同的指针?
现代 x86 CPU 通常具有至少 8 路关联的 L1d 缓存,因此即使在所有数组相对于 4k 边界(即页面大小和通常的 L1d 别名步幅)具有相同偏移的最坏情况下也是如此。不过,Skylake 客户端 L2 仅是 4 路关联,因此不幸的别名可能会导致一些冲突和额外的丢失。
一个循环中 4 个读取流和 2 个写入流通常没问题,包括 L2 预取器。(例如,Intel 的 L2 预取器支持每 4k 页 1 个前向流和 1 个后向流,IIRC。因此,如果数组太小以至于多个数组中的相同索引位于同一 4k 页内,则可能会产生不利影响。)
有关多个加载/存储流的一些详细信息,请参阅For 循环效率:合并循环。(加载和存储竞争相同的 LFB。例如,Skylake 有 12 个,因此它可以跟踪往返 L2 和更远距离的 12 个未完成的缓存行。来自 L2 离开核心的“超级队列”跟踪请求有一些更多条目。)
如果您担心空间局部性,则可以将数据分组为交替的 16 个实数浮点数和 16 个虚数浮点数。例如struct {float real[16], imag[16]};
,如果您__m512
将来想使用向量。
或者,如果您将来可以轻松更改布局,那么现在使用 8 个浮点组,以便 8 个复数适合一个64 字节缓存行(如果您对齐结构),特别是如果您进行任何随机访问。因此,如果元素数量不是 16 的倍数,则浪费的空间会更少。
如果您可以用结果覆盖A
或之一,而不是使用单独的,这将节省一些内存带宽并减少 TLB 页数的占用。(存储到冷内存将导致加载(RFO = 读取所有权)以及最终驱逐脏数据。)B
C
这也意味着需要跟踪的指针减少了。(对于 x86-64 来说这不是一个大问题;它有 15 个通用整数寄存器,与其向量寄存器分开,因此对于整数循环控制来说已经足够了。)