使用YMM向量寄存器将vec4[idx[i]] * scalar[i]求和

3
我正在尝试优化以下代码:sum{vec4[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寄存器的组合所抵消?


2
英特尔的MKL库有一个专门用于此操作的函数:cblas?_ddoti,请参见https://software.intel.com/en-us/mkl-developer-reference-c-cblas-doti。我认为这个函数已经被英特尔高度优化了。因此,您可以直接使用它,或查看其(汇编)源代码--前提是您拥有英特尔编译器许可证。 - Daniel Junglas
在我的情况下,sdoti不需要xy数组具有相同数量的元素吗?实际上,x中的元素数量是y中的四分之一。 - Avi Ginsburg
vec4的大小是多少,或者indexarray的范围是什么?首先将scalar[i]累加到一个临时数组中(每个值在temp[indexarray[i]]处),然后在最后计算一个简单的矩阵向量积可能更有效,除非indexarray中的条目非常稀疏。 - chtz
@chtz vec4 数量在几千个左右(少于1万)。indexarray 是稀疏的(我没有一个确切的数字可以给你,但我的直觉是大约5-10%是满的),但索引倾向于聚集在一起。 - Avi Ginsburg
@DanielJunglas 我认为Avi实际上想要的是一个密集的4 x n矩阵乘以稀疏向量的结果。对于Avi的后续问题:indexarray中是否有重复项?条目是否已排序?如果值聚集在一起,将它们存储在2、4或8个块中是否可行?(例如,将一个起始索引i与值s[i]s[i+7]组合在一起)这将允许在内部循环中手动调整固定大小的矩阵-向量乘积。 - chtz
显示剩余6条评论
1个回答

3
但使用_mm256_set_m128会导致2x3的延迟损失,但这个延迟不在关键路径中,它只是准备FMA的输入之一。对于每个独立的值,做更多的洗牌的问题在于吞吐量。

延迟只真正影响循环传递依赖链通过sum,这既是FMA的输入又是输出。

仅取决于和存储器内容的输入可以通过乱序执行并行处理多次迭代。


如果使用256位向量可能效果更好,但无论如何编写源代码(_mm256_set_m128不是真正的指令),它都可能存在前端或1个时钟洗牌吞吐率的瓶颈。你需要将其编译为128位负载,然后使用vinsertf128 ymm,ymm,[mem],1插入向量的高半部分。vinsertf128会产生一个洗牌uop成本。

如果使用128位寄存器遇到延迟瓶颈,最好只是使用多个累加器,这样(在Haswell上)最多可同时执行10个FMA:5c延迟* 0.5c吞吐量。在结尾处将它们相加。 为什么Haswell上mulss只需要3个周期,而不同于Agner的指令表?


_mm256_set_m128 不是编译成 vinsertf128 吗? - Avi Ginsburg
@AviGinsburg:在这种情况下,它将首先使用vmovups。如果两个输入恰好在内存中是连续的,则可以编译为单个256位加载。或者如果两个输入相同,则(希望)编译为单个128位广播加载。编译器也可以选择将其编译为vperm2f128,如果两个输入都在寄存器中,但通常对于该情况来说,vinsertf128是最好的选择。 - Peter Cordes
谢谢。我决定使用你的Y解决方案来解决我的X问题(选择多个累加器)。这并不排斥我所问的问题,但这样做更有可能遇到不可避免的缓存未命中问题。 - Avi Ginsburg
@AviGinsburg:是的,你甚至可以同时使用2个__m128和1个__m256的混合,或者类似于4x vec4的展开。但是,128位向量可以通过vbroadcastss进行标量加载并且具有内存源操作数的FMA来编译非常高效。 (或者从scalar[i]进行128位加载,然后将其四路洗牌,以减少对负载端口的压力,以防止缓存未命中造成瓶颈,或者通过帮助准备聚集负载地址更快地增加内存并行性。) - Peter Cordes
我选择了8个累加器。如果我最终获得了x4或更高的提升,我将合并__m256。虽然,我也喜欢你的单次加载和四次洗牌的想法,所以我可能会先添加那个。 - Avi Ginsburg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接