用 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],其中vec4float[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 不需要xy 数组具有相同数量的元素吗?在我的例子中,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]的主要内容,如果未能解决你的问题,请参考以下文章

有效地将 YMM 寄存器的低 64 位设置为常数

测试 256 位 YMM AVX 寄存器为零的最有效/惯用方法

有没有更有效的方法将 4 个连续的双精度广播到 4 个 YMM 寄存器中?

使用 NEON 在 ARM 汇编中对四字向量中的所有元素求和

有没有办法将 8bitX32 ymm 寄存器右/左洗牌 N 个位置(c++)

测试 256 位 YMM AVX 寄存器元素是不是等于或小于零的最有效方法