用 YMM 向量寄存器求和 vec4[idx[i]] * scalar[i]
Posted
技术标签:
【中文标题】用 YMM 向量寄存器求和 vec4[idx[i]] * scalar[i]【英文标题】:Summing vec4[idx[i]] * scalar[i] with YMM vector registers 【发布时间】:2019-05-15 07:28:35 【问题描述】:我正在尝试优化以下sumvec4[indexarray[i]] * scalar[i]
,其中vec4
是float[4]
,scalar
是浮点数。对于 128 位寄存器,这归结为
sum = _mm_fmadd_ps(
_mm_loadu_ps(vec4[indexarray[i]]),
_mm_set_ps1(scalar[i]),
sum);
如果我想在 256 位寄存器上执行 FMA,我必须这样做
__m256 coef = _mm256_set_m128(
_mm_set_ps1(scalar[2 * i + 0]),
_mm_set_ps1(scalar[2 * i + 1]));
__m256 vec = _mm256_set_m128(
_mm_loadu_ps(vec4[indexarray[2 * i + 0]]),
_mm_loadu_ps(vec4[indexarray[2 * i + 1]]));
sum = _mm256_fmadd_ps(vec, coef, sum);
随着随机播放并在末尾添加以求上车道和下车道的总和。
理论上,我从单个 FMA 中获得了 5 的延迟(假设 Haswell 架构),但从 _mm256_set_m128
中减少了 2x3 的延迟。
有没有办法使用 ymm 寄存器使这更快,或者单个 FMA 的所有收益都会因组合 xmm 寄存器而抵消?
【问题讨论】:
英特尔的 MKL 库有一个函数可以用于该操作:cblas?_ddoti,参见software.intel.com/en-us/mkl-developer-reference-c-cblas-doti。我怀疑这个功能是由英特尔高度优化的。因此,您可以直接使用它,也可以查看它的(汇编)源代码——前提是您拥有英特尔编译器许可证。 难道sdoti
不需要x
和y
数组具有相同数量的元素吗?在我的例子中,x
中的元素数量是y
的四分之一。
不,我指的函数是sparse版本。您可以在页面的“描述”部分看到这一点。一个向量是密集的,而另一个向量是稀疏的。我认为该函数完全执行您的操作。
vec4
有多大,或者indexarray
的范围是多少?首先将scalar[i]
汇总到一个临时数组(temp[indexarray[i]]
处的每个值)并在最后计算一个简单的矩阵向量积可能更有效——除非indexarray
中的条目非常稀疏。
@chtz vec4
数以千计(少于 10k)。 indexarray
是稀疏的(我没有确切的数字给你,但我的直觉大约是 5-10%),但索引往往会聚集在一起。
【参考方案1】:
但在
_mm256_set_m128
的延迟上损失了 2x3
不,延迟不在关键路径上;这是为 FMA 准备输入的一部分。为每个独立的i
值做更多的洗牌的问题是吞吐量。
延迟只对通过sum
的循环携带依赖链真正重要,它既是 FMA 的输入又是 FMA 的输出。
仅依赖于i
和内存内容的输入可以通过乱序执行跨多次迭代并行处理。
不过,您可能仍然领先,构建 256 位向量。无论您编写源代码(_mm256_set_m128
不是真正的指令),它都可能会成为前端或每时钟 1 次随机播放吞吐量的瓶颈。您希望它编译为 128 位加载,然后 vinsertf128 ymm, ymm, [mem], 1
插入向量的高半部分。 vinsertf128
确实要花很多钱。
如果您在 128 位寄存器的延迟上遇到瓶颈,最好只使用多个累加器,这样(在 Haswell 上)最多可以同时运行 10 个 FMA:5c 延迟 * 0.5c 吞吐量。在最后添加它们。 Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables?
【讨论】:
_mm256_set_m128
不能编译成vinsertf128
?
@AviGinsburg:在这种情况下,它会首先出现vmovups
。如果两个输入碰巧在内存中是连续的,它可以编译为单个 256 位加载。或者,如果两个输入相同,它将(希望)编译为单个 128 位广播负载。如果两个输入都在寄存器中,编译器也可以选择将其编译为vperm2f128
,但通常vinsertf128
也最适合这种情况。
谢谢。我决定使用你的 Y 解决方案来解决我的 X 问题(选择多个累加器)。并不是说这与我所问的问题相互排斥,但我更有可能以这种方式遇到不可避免的缓存未命中问题。
@AviGinsburg:是的,你甚至可以同时使用 2x __m128
和 1x __m256
或其他东西,然后展开 4x vec4
。但是 128 位向量可以非常高效地编译,vbroadcastss
用于标量负载,内存源操作数用于 FMA。 (或者来自scalar[i]
的 128 位负载,您可以 4 种方式进行混洗,以减少负载端口的压力,以防缓存未命中成为瓶颈,或者可能通过帮助更快地收集负载地址来增加内存并行度。)
我选择了 8 个累加器。如果我最终获得了 x4 或更多的提升,我将合并 __m256
。虽然,我也喜欢你单次加载和四次随机播放的想法,所以我可能会先添加。以上是关于用 YMM 向量寄存器求和 vec4[idx[i]] * scalar[i]的主要内容,如果未能解决你的问题,请参考以下文章
测试 256 位 YMM AVX 寄存器为零的最有效/惯用方法
有没有更有效的方法将 4 个连续的双精度广播到 4 个 YMM 寄存器中?
使用 NEON 在 ARM 汇编中对四字向量中的所有元素求和