我想通过使用直接算法将两个矩阵相乘一次:
template <typename T>
void multiplicate_straight(T ** A, T ** B, T ** C, int sizeX)
{
T ** D = AllocateDynamicArray2D<T>(sizeX, sizeX);
transpose_matrix(B, D,sizeX);
for(int i = 0; i < sizeX; i++)
{
for(int j = 0; j < sizeX; j++)
{
for(int g = 0; g < sizeX; g++)
{
C[i][j] += A[i][g]*D[j][g];
}
}
}
FreeDynamicArray2D<T>(D);
}
和一次使用SSE功能。为此,我创建了两个函数:
template <typename T>
void SSE_vectormult(T * A, T * B, int size)
{
__m128d a;
__m128d b;
__m128d c;
#ifdef linux
double A2[2], B2[2], C[2] __attribute__ ((aligned(16)));
#endif
#ifdef _WIN32
__declspec(align(16)) double A2[2], B2[2], C[2];
#endif
for(int i = 0; i < size; i+=2)
{
//std::cout << "In SSE_vectormult: i is: " << i << '\n';
A2[0] = A[i];
B2[0] = B[i];
A2[1] = A[i+1];
B2[1] = B[i+1];
//std::cout << "Values from A and B written to A2 and B2\n";
a = _mm_load_pd(A2);
b = _mm_load_pd(B2);
//std::cout << "Values converted to a and b\n";
c = _mm_mul_pd(a,b);
_mm_store_pd(C, c);
A[i] = C[0];
A[i+1] = C[1];
};
}
和
template <typename T>
void multiplicate_SSE(T ** A, T ** B, T ** C, int sizeX)
{
// std::cout << "Entered SSE-Function\n";
T ** D = AllocateDynamicArray2D<T>(sizeX, sizeX);
T * tmp = AllocateDynamicArray1D<T>(sizeX);
T * tmp2 = AllocateDynamicArray1D<T>(sizeX);
//std::cout << "Matrices allocated\n";
transpose_matrix<T>(B, D,sizeX);
//std::cout << "Matrix B transposed\n";
for(int i = 0; i < sizeX; i++)
{
for(int j = 0; j < sizeX; j++)
{
extract_row<T>(A,tmp, i, sizeX);
// std::cout << "Row from A extracted\n";
//print_vector(tmp, sizeX);
extract_row<T>(D, tmp2, j, sizeX);
// std::cout << "Row from D extracted\n";
//print_vector(tmp2, sizeX);
SSE_vectormult<T>(tmp, tmp2, sizeX);
// std::cout << "Vectors multiplicated\n";
//print_vector(tmp, sizeX);
C[i][j] = add_vector(tmp, sizeX);
// std::cout << "Written value to C\n";
// std::cout << "j is " << j << " and i is " << i << '\n';
}
}
// std::cout << "Loop finished\n";
FreeDynamicArray2D<T>(D);
//std::cout << "Freed D\n";
//FreeDynamicArray1D<T>(tmp);????
// std::cout << "Freed tmp\n";
FreeDynamicArray1D<T>(tmp2);
// std::cout << "Everything freed, returning\n";
}
但后来我遇到了几个问题:一方面,当我想在multiplicate_SSE()中释放tmp数组时,标记有几个问号,我得到错误“_BLOCK_TYPE_IS_VALID”。我考虑过两次释放相同空间的可能性,所以我对此进行了取消注释(但是我认为通过这种方式我得到了内存泄漏?)。现在,当我将两个函数的性能与相同的矩阵进行比较时,两个1024x1024矩阵的SSE函数需要比直接法多四倍左右。
如何重写我的SSE-Function以获得更好的性能(我以前从未使用过SSE),如何修复内存泄漏?
谢谢!
答案 0 :(得分:4)
通过在标量代码中进行转置,你有正确的想法,但在使用SSE时你不需要完全转置。
让我们坚持浮动(SGEMM)。你想用SSE做什么就是一次做四个点产品。你想要C = A*B
。我们来看一个8x8矩阵。我们假设B
是:
(0 1 2 3) ( 4 5 6 7)
(8 9 10 11) (12 13 14 15)
(16 17 18 19) (20 21 22 23)
(24 25 26 27) (28 29 30 31)
(32 33 34 35) (36 37 38 39)
(40 41 42 43) (44 45 46 47)
(48 49 50 51) (52 53 54 55)
(56 57 58 59) (60 61 62 63)
所以使用SSE你可以:
C[0][0] C[0][1] C[0][2] C[0][3] =
A[0][0]*(0 1 2 3) + A[0][1]*(8 9 10 11) + A[0][2]*(16 17 18 19)...+ A[0][7]*(56 57 58 59)
一次可以获得四个点产品。问题是您必须向下移动B
中的列,并且值不在同一缓存行中。 如果每个宽度为4的列在内存中是连续的,那会更好。因此,不要对每个元素进行转置,而是转换宽度为4 的条带,如下所示:
(0 1 2 3)( 8 9 10 11)(16 17 18 19)(24 25 26 27)(32 33 34 35)(40 41 42 43)(48 49 50 51)(56 57 58 59)
(4 5 6 7)(12 13 14 15)(20 21 22 23)(28 29 30 31)(36 37 38 39)(44 45 46 47)(52 53 54 55)(60 61 62 63)
如果您将括号中的四个值中的每一个视为一个单位,则相当于将8x2矩阵转换为2x8矩阵。现在请注意{{1}的宽度为4的列在记忆中是连续的。这对缓存更友好了。对于8x8矩阵,这不是一个真正的问题,但例如使用1024x1024矩阵。有关如何执行此操作,请参阅下面的代码。对于AVX,您可以移调宽度为8的条带(这意味着您无法对8x8矩阵进行任何操作)。对于double,宽度为2,SSE为4,AVX为4。
假设矩阵适合缓存,这应该比标量代码快四倍。但是,对于大型矩阵,此方法仍然是内存绑定的,因此您的SSE代码可能不会比标量代码快得多(但它不应该更糟)。
但是,如果你使用循环平铺并重新排列tile中的矩阵(适合L2缓存)而不是整个矩阵矩阵,则乘法是计算绑定的,即使对于非常大的矩阵也不是内存绑定t适合L3缓存。这是另一个主题。
编辑:一些(未经测试的)代码与您的标量代码进行比较。我将循环展开了2。
B
请注意,void SGEMM_SSE(const float *A, const float *B, float *C, const int sizeX) {
const int simd_width = 4;
const int unroll = 2;
const int strip_width = simd_width*unroll
float *D = (float*)_mm_malloc(sizeof(float)*sizeX*sizeX, 16);
transpose_matrix_strip(B, D,sizeX, strip_width); //tranpose B in strips of width eight
for(int i = 0; i < sizeX; i++) {
for(int j = 0; j < sizeX; j+=strip_width) {
float4 out_v1 = 0; //broadcast (0,0,0,0)
float4 out_V2 = 0;
//now calculate eight dot products
for(int g = 0; g < sizeX; g++) {
//load eight values rrom D into two SSE registers
float4 vec4_1.load(&D[j*sizeX + strip_width*g]);
float4 vec4_2.load(&D[j*sizeX + strip_width*g + simd_width]);
out_v1 += A[i][g]*vec4_v1;
out_v2 += A[i][g]*vec4_v2;
}
//store eight dot prodcuts into C
out_v1.store(&C[i*sizeX + j]);
out_v2.store(&C[i*sizeX + j + simd_width]);
}
}
_mm_free(D);
}
void transpose_matrix_strip(const float* A, float* B, const int N, const int strip_width) {
//#pragma omp parallel for
for(int n=0; n<N*N; n++) {
int k = strip_width*(n/N/strip_width);
int i = (n/strip_width)%N;
int j = n%strip_width;
B[n] = A[N*i + k + j];
}
}
现在增加8。更多展开可能会有所帮助。如果您想使用内在函数,可以使用j
,_mm_load_ps
,_mm_store_ps
(对于广播,例如_mm_set1_ps
),_mm_set1_ps(A[i][g])
和_mm_add_ps
。就是这样。
答案 1 :(得分:1)
我认为这应该与第一个带SSE的循环做同样的事情,假设sizeX是2的倍数,并且内存是16字节对齐的。
通过展开循环并使用最后添加的多个临时变量,可以获得更高的性能。您也可以尝试使用AVX和新的Fused Multiply Add instruction。
template <typename T>
void multiplicate_SSE2(T ** A, T ** B, T ** C, int sizeX)
{
T ** D = AllocateDynamicArray2D<T>(sizeX, sizeX);
transpose_matrix(B, D,sizeX);
for(int i = 0; i < sizeX; i++)
{
for(int j = 0; j < sizeX; j++)
{
__m128d temp = _mm_setzero_pd();
for(int g = 0; g < sizeX; g += 2)
{
__m128d a = _mm_load_pd(&A[i][g]);
__m128d b = _mm_load_pd(&D[j][g]);
temp = _mm_add_pd(temp, _mm_mul_pd(a,b));
}
// Add top and bottom half of temp together
temp = _mm_add_pd(temp, _mm_shuffle_pd(temp, temp, 1));
_mm_store_sd(temp, &C[i][j]); // Store one value
}
}
FreeDynamicArray2D<T>(D);
}