我正在开发基于Intel指令集(AVX,FMA等)的高性能算法。当数据按顺序存储时,我的算法(内核)运行良好。但是,现在我面临一个大问题,但没有找到解决方法或解决方案: see 2D Matrix
int x, y; x = y = 4096;
float data[x*y]__attribute__((aligned(32)));
float buffer[y]__attribute__((aligned(32)));
/* simple test data */
for (i = 0; i < x; i++)
for (j = 0; j < y; j++)
data[y*i+j] = y*i+j; // 0,1,2,3...4095, | 4096,4097, ... 8191 |...
/* 1) Extract the columns out of matrix */
__m256i vindex; __m256 vec;
vindex = _mm256_set_epi32(7*y, 6*y, 5*y, 4*y, 3*y, 2*y, y, 0);
for(i = 0; i < x; i+=8)
{
vec = _mm256_i32gather_ps (&data[i*y], vindex, 4);
_mm256_store_ps (buffer[i], vec);
}
/* 2) Perform functions */
fft(buffer, x) ;
/*3) write back buffer into matrix*/
/* strided write??? ...*/
我想找到一种非常有效的方法来执行以下操作:
从矩阵中提取列:col1 = 0,4096,8192,... col2 = 1,4097,8193,... 我尝试了collect_ps,这真的很慢。
对提取的列执行我的高效算法...
有什么特别的把戏吗? 如何使用Intel指令集读取和写入大步距(例如4096)?
或者是否有任何内存操作选项可以将列从矩阵中取出?
谢谢!
答案 0 :(得分:4)
[对于行主要数据,SIMD对行的访问速度快,而对列的访问速度慢]
是的,这就是x86-64和类似体系结构的本质。访问内存中的连续数据很快,但是访问分散的数据(无论是随机的还是规则的模式)则很慢。这是拥有处理器缓存的结果。
有两种基本方法:将数据复制到便于使用更好访问模式的新顺序,或者按允许使用更好访问模式的顺序进行计算。
不,没有任何经验法则或千篇一律的技巧可以使一切正常。实际上,即使比较不同的实现也很棘手,因为存在许多复杂的交互(从缓存等待时间到操作交错,再到缓存和内存访问模式),因此结果在很大程度上取决于特定的硬件和手头的数据集。
让我们看一下典型的示例情况,矩阵矩阵乘法。假设我们使用标准的C行主要数据顺序将两个5×5矩阵相乘(c = a×b):
c00 c01 c02 c03 c04 a00 a01 a02 a03 a04 b00 b01 b02 b03 b04
c05 c06 c07 c08 c09 a05 a06 a07 a08 a09 b05 b06 b07 b08 b09
c10 c11 c12 c13 c14 = a10 a11 a12 a13 a14 × b10 b11 b12 b13 b14
c15 c16 c17 c18 c19 a15 a16 a17 a18 a19 b15 b16 b17 b18 b19
c20 c21 c22 c23 c24 a20 a21 a22 a23 a24 b20 b21 b22 b23 b24
如果我们将结果写为具有五个分量的垂直SIMD矢量寄存器,则有
c00 a00 b00 a01 b05 a02 b10 a03 b15 a04 b20
c01 a00 b01 a01 b06 a02 b11 a03 b16 a04 b21
c02 = a00 × b02 + a01 × b07 + a02 × b12 + a03 × b17 + a04 × b22
c03 a00 b03 a01 b08 a02 b13 a03 b18 a04 b23
c04 a00 b04 a01 b09 a02 b14 a03 b19 a04 b24
c05 a05 b00 a06 b05 a07 b10 a08 b15 a09 b20
c06 a05 b01 a06 b06 a07 b11 a08 b16 a09 b21
c07 = a05 × b02 + a06 × b07 + a07 × b12 + a08 × b17 + a09 × b22
c08 a05 b03 a06 b08 a07 b13 a08 b18 a09 b23
c09 a05 b04 a06 b09 a07 b14 a08 b19 a09 b24
,依此类推。换句话说,如果c
与b
的顺序相同,则我们可以对c
和b
使用具有连续存储内容的SIMD寄存器,而只收集{{1 }}。此外,a
的SIMD寄存器的所有组件都具有相同的值。
但是请注意,a
寄存器对b
的所有五行重复。因此,最好将c
初始化为零,然后对具有相同c
SIMD寄存器的乘积进行加法运算:
b
如果我们首先对c00 a00 b00 c05 a05 b00 c10 a10 b00 c15 a15 b00 c20 a20 b00
c01 a00 b01 c06 a05 b01 c11 a10 b01 c16 a15 b01 c21 a20 b01
c02 += a00 × b02, c07 += a05 × b02, c12 += a10 × b02, c17 += a15 × b02, c22 += a20 × b02
c03 a00 × b03 c08 a05 b03 c13 a10 b03 c18 a15 b03 c23 a20 b03
c04 a00 × b04 c09 a05 b04 c14 a10 b04 c19 a15 b04 c24 a20 b04
进行转置,那么a
的SIMD向量寄存器也将从连续的存储位置中获取值。实际上,如果a
足够大,则线性化a
的内存访问模式也可以提供足够大的速度提升,从而可以更快地进行转置副本(对浮点数使用a
,并且uint32_t
用于双打;即根本不使用SIMD或浮点进行转置,而只是按转置顺序复制存储空间。
请注意,列主要数据顺序(即,与上述数据相比转置的数据顺序)的情况非常相似。这里有很深的对称性。例如,如果uint64_t
和c
具有相同的数据顺序,而b
具有相反的数据顺序,则可以有效地对矩阵乘积进行SIMD矢量化,而不必复制任何数据。只有求和不同,这取决于数据顺序,并且矩阵乘法不是可交换的(a×b!= b×a)。
显然,主要的缺点是SIMD向量寄存器的大小是固定的,因此,只能使用部分行,而不是像上面的示例那样使用完整的行作为寄存器。 (如果结果中的列数不是SIMD寄存器宽度的倍数,则也要担心该部分向量。)
SSE和AVX具有相对大量的寄存器(8、16或32,具体取决于所使用的扩展集),并且取决于特定的处理器类型,它们可能能够同时或以如果不相关的向量操作被交错,则延迟最少。因此,甚至可以选择一次操作一个块的宽度,以及该块是扩展矢量还是块子矩阵,都需要讨论,测试和比较。
那么如何使用SIMD最有效地进行矩阵矩阵乘法?
就像我说的那样,这取决于数据集。恐怕没有简单的答案。
(选择最有效的方法)的主要参数是被乘数和结果矩阵的大小和存储顺序。
如果您计算两个以上大小不同的矩阵的乘积,它将变得更加有趣。这是因为操作次数取决于产品的顺序。
你为什么这么气our?
实际上我不是。以上所有这些意味着没有太多的人可以处理这种复杂性并保持理智和高效,因此有很多未发现的方法,并且可以在现实世界中获得很多收益。
即使我们忽略编译器提供的SIMD内部函数(在这种情况下为a
),我们也可以在设计内部数据结构时应用上面的逻辑,从而使我们使用的C编译器有最佳的机会对计算进行矢量化为我们。 (不过,它们还不是很擅长。就像我说的那样,复杂的东西。有些人喜欢Fortran比C强,因为它的表达式和规则使Fortran编译器更容易优化和矢量化它们。)
如果这是简单还是容易的话,那么解决方案将是众所周知的。但事实并非如此,因为事实并非如此。但这并不意味着这是不可能的或我们无法实现的。它的全部意思是,足够聪明的开发人员尚未投入足够的精力来解决这个问题。
答案 1 :(得分:2)
如果您可以并行运行8(或16 1 )列上的算法,则一次常规AVX加载可以将8列数据捕获到一个向量中。然后,另一个负载可以从所有这些列中获取下一行。
这具有您无需在向量内随机播放的优点;一切都是纯垂直的,并且每一列的连续元素在不同的向量中。
如果这是像对列求和一样的减少,那么您将并行产生8个结果。如果您要随时更新列,那么您一次要写8个列的结果向量,而不是一个列的8个元素的向量。
脚注1:16 float
列= 64字节= 1个完整的缓存行=两个AVX向量或一个AVX512向量。一次读/写完整的缓存行比一次遍历 one 列要好得多,尽管通常比访问连续的缓存行要差。尤其是如果跨度大于4k页面,则硬件预取可能无法很好地锁定它。
显然,为此请确保您的数据以64对齐,并且行的跨度也应为64字节的倍数。如果需要,请填充行尾。
如果在循环返回以读取列8..15的第二个32字节向量之前从L1d逐出第一行,则一次仅执行一个AVX向量(半个缓存行)将是不好的。 >
其他警告:
4k别名可能是一个问题:存储和来自相距4kiB的倍数的地址的负载不会立即被检测为不重叠,因此负载会被商店。这样可以大大减少CPU可以利用的并行度。
4k的跨步也可能导致高速缓存中的冲突未命中,如果您要触摸很多别名相同的行。因此,就地更新数据可能仍会为存储造成高速缓存未命中的情况,因为在加载和处理之后,在存储准备提交之前可能会逐出行。 如果您的行距是2的大幂,则很可能会出现问题。如果最终出现问题,请在这种情况下分配更多内存,并在行上填充未使用的元素最后,因此存储格式永远不会具有2行跨度的强大功能。
L2高速缓存中的相邻行预取(英特尔CPU)可能会尝试在您有空闲带宽的情况下填写您触摸的每一行对。这可能最终会清除有用的数据,尤其是在您接近别名和/或L2容量的情况下。但是,如果您没有达到这些限制,那可能是一件好事,当您遍历接下来的16列时会有所帮助。
答案 2 :(得分:0)
数据应在存储器中一行一行地存储。 由于C并不真正在乎它是数组还是矩阵,因此您可以使用
访问元素 for(int i=0;i<columncount;i++)
data[i*LENGTH + desired_column];
您现在可以存储数据甚至更好的地址以将其提供给工作人员功能。如果您使用地址,矩阵中的值将更改,因此您无需将其写回。