我正在尝试使用GCC向量扩展(https://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html)来加速矩阵乘法。我们的想法是使用SIMD指令一次乘以并添加四个浮点数。下面列出了一个最小的工作示例。当将(M = 10,K = 12)矩阵乘以(K = 12,N = 12)矩阵时,该示例工作正常。但是,当我更改参数(比如N = 9)时,我会遇到分段错误。
我怀疑这是由于内存对齐问题。根据我的理解,当使用SIMD作为16字节的向量(在这种情况下为float4)时,目标存储器地址应该是16的倍数。已经讨论了SIMD指令的存储器对齐问题。 (例如Relationship between SSE vectorization and Memory alignment)。在下面的示例中,当& b(0,0)为0x810e10时,& b(1,0)为0x810e34,这不是16的倍数。
我的问题是,
最小工作示例
#include <iostream>
#include <cstdlib>
#include <stdio.h>
#include <cstring>
#include <assert.h>
#include <algorithm>
using namespace std;
typedef float float4 __attribute__((vector_size (16)));
static inline void * alloc64(size_t sz) {
void * a = 0;
if (posix_memalign(&a, 64, sz) != 0) {
perror("posix_memalign");
exit(1);
}
return a;
}
struct Mat {
size_t m,n;
float * a;
Mat(size_t m_, size_t n_, float f) {
m = m_;
n = n_;
a = (float*) malloc(sizeof(float) * m * n);
fill(a,a + m * n,f);
}
/* a(i,j) */
float& operator()(long i, long j) {
return a[i * n + j];
}
};
Mat operator* (Mat a, Mat b) {
Mat c(a.m, b.n,0);
assert(a.n == b.m);
for (long i = 0; i < a.m; i++) {
for(long k = 0; k < a.n; k++){
float aa = a(i,k);
float4 a4 = {aa,aa,aa,aa};
long j;
for (j = 0; j <= b.n-4; j+=4) {
*((float4 *)&c(i,j)) = *((float4 *)&c(i,j)) + a4 * (*(float4 *)&b(k,j));
}
while(j < b.n){
c(i,j) += aa * b(k,j);
j++;
}
}
}
return c;
}
const int M = 10;
const int K = 12;
const int N = 12;
int main(){
Mat a(M,K,1);
Mat b(K,N,1);
Mat c = a * b;
for(int i = 0; i < M; i++){
for(int j = 0; j < N; j++)
cout << c(i,j) << " ";
cout << endl;
}
cout << endl;
}
答案 0 :(得分:0)
在我的理解中,当使用SIMD用于16字节(in 这种情况下float4),目标内存地址应该是倍数 16。
在x64处理器上这是不正确的。有些指令需要对齐,但您可以很好地从未对齐的存储器位置写入和读取SIMD寄存器而不会受到惩罚,并且使用正确的指令绝对安全。
我是否因为内存对齐而遇到了段错误 问题
是
但它与SIMD指令无关。在C / C ++中,以你的方式编写*((float4 *)&c) = ...
是未定义的行为,并且肯定会崩溃,但你可以在没有向量化的情况下重现问题......在适当的情况下,以下基本代码将崩溃。
char * c = ...
*(int *) c = 1;
有谁能告诉我如何轻松解决问题?我已经想过了 使用二维数组而不是一个数组,但我不想要 这样做是为了不改变其余的代码。
典型的解决方法是使用memcpy
。我们来看一个代码示例......
#include <string.h>
typedef float float4 __attribute__((vector_size (16)));
void writeover(float * x, float4 y) {
*(float4 * ) x = y;
}
void writeover2(float * x, float4 y) {
memcpy(x,&y,sizeof(y));
}
使用clang ++,这两个函数被编译为vmovaps
和vmovups
。这些是等效的指令,但如果指针未在sizeof(float4)
上对齐,则第一个指令会崩溃。它们在最近的硬件上功能非常快。
重点是,您通常可以依靠memcpy
生成几乎最快的代码。当然,您获得的开销(如果有的话)将取决于您使用的编译器。
如果您确实遇到了性能问题,那么您可以使用英特尔内在函数或汇编代替......但memcpy
很有可能为您提供服务。
另一种解决方法是仅使用float4 *
指针。这会强制所有矩阵具有可被4整除的维度,但如果用剩余的填充剩余部分,则可能会得到简单且非常快的代码。