【发布时间】:2020-01-28 13:33:45
【问题描述】:
我正在使用 avx512 和矩阵乘法,但我一定做错了,因为当我尝试使用 _mm512_store_pd 存储我的结果时,我的表现很糟糕。
以下是相关的sn-ps代码,首先是我正在使用的数据结构以及我如何初始化它:
typedef struct {
double* values;
int nb_l;
int nb_c;
} matrix;
matrix* alloc_matrix(int nb_l, int nb_c){
matrix* tmp_matrix = (matrix*)malloc(sizeof(matrix));
tmp_matrix->values = (double*)aligned_alloc(64, sizeof(double) * nb_l * nb_c);
tmp_matrix->nb_l = nb_l;
tmp_matrix->nb_c = nb_c;
return tmp_matrix;
}
这就是我如何尝试将在我的代码中其他地方初始化的两个矩阵相乘:
matrix* mult_matrix(matrix* A, matrix* B){
/* avx512 */
matrix* res_matrix = zero_matrix(A->nb_l, B->nb_c);
double* res_ptr; // start index of the current line in res_matrix
double* B_ptr; // start index of the current line in B
__m512d A_broadcast, B_l_8, res_ptr_8;
for (unsigned int idx_A = 0; idx_A < A->nb_l * A-> nb_c; idx_A++){
// broadcast current value of A eight times
A_broadcast = _mm512_set1_pd(A->values[idx_A]);
res_ptr = res_matrix->values + (idx_A / A->nb_c) * B->nb_c;
B_ptr = B->values + (idx_A % A->nb_c) * B->nb_c;
for (unsigned int offset_B = 0; offset_B < B->nb_c; offset_B+=8){
B_l_8 = _mm512_load_pd(&B_ptr[offset_B]);
res_ptr_8 = _mm512_load_pd(&res_ptr[offset_B]);
_mm512_store_pd(
&res_ptr[offset_B] ,
_mm512_fmadd_pd(A_broadcast, B_l_8, res_ptr_8)
);
}
}
return res_matrix;
结果还可以,但 _mm512_store_pd 占用了约 90% 的执行时间,实际上这个 avx512 代码几乎不比它的非 avx 版本快。
我已经尝试了所有我能想到的方法,但我不知道为什么我的代码表现如此令人失望。你有什么想法吗?
谢谢。
编辑 1
这里是非avx代码
matrix* res_matrix = zero_matrix(A->nb_l, B->nb_c);
double* res_ptr; // start index of the current line in res_matrix
double* B_ptr; // start index of the current line in B
for (unsigned int idx_A = 0; idx_A < A->nb_l * A-> nb_c; idx_A++){
res_ptr = res_matrix->values + (idx_A / A->nb_c) * B->nb_c;
B_ptr = B->values + (idx_A % A->nb_c) * B->nb_c;
for (unsigned int offset_B = 0; offset_B < B->nb_c; offset_B++){
res_ptr[offset_B] += A->values[idx_A] * B_ptr[offset_B];
}
}
return res_matrix;
所有矩阵都是512x512的随机矩阵,每次乘法重复50次,平均运行时间。
最后,为了测试我的代码的 avx 和 non_avx 版本,下面的 sn-p 应该没问题。我使用以下选项使用 gcc 8.3.0 编译它: gcc -Ofast -mavx -mavx512f -m64 -mfpmath=sse -mfma -flto -funroll-loops matrix_minimal.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <string.h>
#include <immintrin.h>
typedef struct {
double* values;
int nb_l;
int nb_c;
} matrix;
matrix* alloc_matrix(int nb_l, int nb_c){
matrix* tmp_matrix = (matrix*)malloc(sizeof(matrix));
tmp_matrix->values = (double*)aligned_alloc(64, sizeof(double) * nb_l * nb_c);
tmp_matrix->nb_l = nb_l;
tmp_matrix->nb_c = nb_c;
return tmp_matrix;
}
void free_matrix(matrix** to_free){
free((*to_free)->values);
free(*to_free);
}
matrix* zero_matrix(int nb_l, int nb_c){
matrix* z_matrix;
z_matrix = alloc_matrix(nb_l, nb_c);
for (int idx=0; idx < nb_l * nb_c; idx++){
z_matrix->values[idx] = 0.0;
}
return z_matrix;
}
matrix* rand_matrix(int nb_l, int nb_c, double max_abs_val){
static struct timeval seed; //static variables are zeroed at initialization
matrix* rnd_matrix;
rnd_matrix = alloc_matrix(nb_l, nb_c);
if (seed.tv_sec == 0){ //ts_sec will never be zero after gettimeofday, whereas tv_usec could
gettimeofday(&seed, NULL);
srand((unsigned) seed.tv_usec);
}
for (int idx=0; idx < nb_l * nb_c; idx++){
rnd_matrix->values[idx] = max_abs_val * ((double)rand() / RAND_MAX * 2.0 - 1.0);
}
return rnd_matrix;
}
matrix* mult_matrix_avx(matrix* A, matrix* B){
/* pas trop mal en avx512 */
matrix* res_matrix = zero_matrix(A->nb_l, B->nb_c);
double* res_ptr; // start index of the current line in res_matrix
double* B_ptr; // start index of the current line in B
__m512d A_broadcast, B_l_8, res_ptr_8;
for (unsigned int idx_A = 0; idx_A < A->nb_l * A-> nb_c; idx_A++){
A_broadcast = _mm512_set1_pd(A->values[idx_A]); // broadcast current value of A eight times
res_ptr = res_matrix->values + (idx_A / A->nb_c) * B->nb_c;
B_ptr = B->values + (idx_A % A->nb_c) * B->nb_c;
for (unsigned int offset_B = 0; offset_B < B->nb_c; offset_B+=8){
B_l_8 = _mm512_load_pd(&B_ptr[offset_B]);
res_ptr_8 = _mm512_load_pd(&res_ptr[offset_B]);
_mm512_store_pd(&res_ptr[offset_B] , _mm512_fmadd_pd(A_broadcast, B_l_8, res_ptr_8));
}
}
return res_matrix;
}
matrix* mult_matrix(matrix* A, matrix* B){
/* non avx512 */
matrix* res_matrix = zero_matrix(A->nb_l, B->nb_c);
double* res_ptr; // start index of the current line in res_matrix
double* B_ptr; // start index of the current line in B
for (unsigned int idx_A = 0; idx_A < A->nb_l * A-> nb_c; idx_A++){
res_ptr = res_matrix->values + (idx_A / A->nb_c) * B->nb_c;
B_ptr = B->values + (idx_A % A->nb_c) * B->nb_c;
for (unsigned int offset_B = 0; offset_B < B->nb_c; offset_B++){
res_ptr[offset_B] += A->values[idx_A] * B_ptr[offset_B];
}
}
return res_matrix;
}
int main(int argc, char *argv[]){
struct timeval before;
struct timeval after;
matrix* A = rand_matrix(512, 512, 5);
matrix* B = rand_matrix(512, 512, 5);
matrix *C;
gettimeofday(&before, NULL);
for (int j=0; j<50;j++){
C = mult_matrix_avx(A, B);
free_matrix(&C); // we will measure the same overhead here and in the non avx version
}
gettimeofday(&after, NULL);
double delta = ((after.tv_sec - before.tv_sec) * 1000000 +
(after.tv_usec - before.tv_usec))/50;
printf("avx %lf ms\n", delta);
gettimeofday(&before, NULL);
for (int j=0; j<50;j++){
C = mult_matrix(A, B);
free_matrix(&C);
}
gettimeofday(&after, NULL);
delta = ((after.tv_sec - before.tv_sec) * 1000000 +
(after.tv_usec - before.tv_usec))/50;
printf("non avx %lf ms\n", delta);
free_matrix(&A);
free_matrix(&B);
return 0;
}
【问题讨论】:
-
actually this avx512 code is barely faster than its non avx version- 你能发布“非 avx 版本”吗?你能发布你如何衡量绩效吗?您使用的是什么 gcc 版本以及您使用的是什么编译选项?你传递什么参数给alloc_matrix来分配矩阵?你能创建一个完整的minimal reproducible example吗? -
您的代码在很多方面都不是最优的。理想情况下,每次 FMA 操作的负载应该略多于一个,并且在实际将结果存储回来之前要使用更多的 FMA(您需要在寄存器中保留更多的中间结果)。此外,如果您的矩阵不适合 L1,则需要对适合 L1 的子块进行操作。
-
更多
const和restrict不会有什么坏处。
标签: c performance gcc matrix avx512