C语言中矩阵和向量乘法的优化

时间:2018-07-26 13:15:12

标签: c performance optimization linear-algebra matrix-multiplication

我有一个函数,该函数获取3 x 3矩阵和3 x 4000 vector并将它们相乘。 所有计算均以双精度(64位)完成。 该函数被称为3.5百万次,因此应对其进行优化。

#define MATRIX_DIM 3
#define VECTOR_LEN 3000

typedef struct {
    double a;
    double b;
    double c;
} vector_st;

double matrix[MATRIX_DIM][MATRIX_DIM];
vector_st vector[VACTOR_LEN];

inline void rotate_arr(double input_matrix[][MATRIX_DIM], vector_st *input_vector, vector_st *output_vector)
{
    int i;
    for (i = 0; i < VACTOR_LEN; i++) {
        op_rotate_preset_arr[i].a = input_matrix[0][0] * input_vector[i].a + 
                                    input_matrix[0][1] * input_vector[i].b +
                                    input_matrix[0][2] * input_vector[i].c;

        op_rotate_preset_arr[i].b = input_matrix[1][0] * input_vector[i].a + 
                                    input_matrix[1][1] * input_vector[i].b +
                                    input_matrix[1][2] * input_vector[i].c;

        op_rotate_preset_arr[i].c = input_matrix[2][0] * input_vector[i].a + 
                                    input_matrix[2][1] * input_vector[i].b +
                                    input_matrix[2][2] * input_vector[i].c;
    }
}

我全都没有关于如何优化它的想法,因为它是inline,数据访问是顺序的,该函数很短而且很简单。 可以假定向量始终相同,并且只有矩阵会发生变化,这样才能提高性能。

1 个答案:

答案 0 :(得分:1)

一个容易解决的问题是,编译器假定矩阵和输出向量可能互为别名。如第二个函数中的here所示,这将导致生成的代码效率较低,并且明显更大。只需将restrict添加到输出指针即可解决此问题。仅这样做已经可以帮助并使代码免于特定于平台的优化,但是要依靠自动矢量化才能使用过去二十年来发生的性能提高。

显然,自动矢量化对于该任务来说还太不成熟,Clang和GCC都会对数据进行过多的混洗。这在将来的编译器中应该会有所改善,但就目前而言,即使是这样的情况(看起来并非天生就很难),也需要手动帮助,例如这样的(尽管未测试)

void rotate_arr_avx(double input_matrix[][MATRIX_DIM], vector_st *input_vector, vector_st * restrict output_vector)
{
    __m256d col0, col1, col2, a, b, c, t;
    int i;
    // using set macros like this is kind of dirty, but it's outside the loop anyway
    col0 = _mm256_set_pd(0.0, input_matrix[2][0], input_matrix[1][0], input_matrix[0][0]);
    col1 = _mm256_set_pd(0.0, input_matrix[2][1], input_matrix[1][1], input_matrix[0][1]);
    col2 = _mm256_set_pd(0.0, input_matrix[2][2], input_matrix[1][2], input_matrix[0][2]);
    for (i = 0; i < VECTOR_LEN; i++) {
        a = _mm256_set1_pd(input_vector[i].a);
        b = _mm256_set1_pd(input_vector[i].b);
        c = _mm256_set1_pd(input_vector[i].c);
        t = _mm256_add_pd(_mm256_add_pd(_mm256_mul_pd(col0, a), _mm256_mul_pd(col1, b)), _mm256_mul_pd(col2, c));
        // this stores an element too much, ensure 8 bytes of padding exist after the array
        _mm256_storeu_pd(&output_vector[i].a, t);
    }
}

以这种方式编写可以显着改善编译器的处理方式,现在可以将其编译成一个美观而紧密的循环,而不会产生任何废话。以前的代码令人讨厌看,但是有了这个循环,现在看起来像这样(GCC 8.1,启用了FMA),实际上是可读的:

.L2:
    vbroadcastsd    ymm2, QWORD PTR [rsi+8+rax]
    vbroadcastsd    ymm1, QWORD PTR [rsi+16+rax]
    vbroadcastsd    ymm0, QWORD PTR [rsi+rax]
    vmulpd  ymm2, ymm2, ymm4
    vfmadd132pd     ymm1, ymm2, ymm3
    vfmadd132pd     ymm0, ymm1, ymm5
    vmovupd YMMWORD PTR [rdx+rax], ymm0
    add     rax, 24
    cmp     rax, 72000
    jne     .L2

这有一个明显的缺陷:256位AVX向量的4个双精度时隙中只有3个被实际使用。如果将向量的数据格式更改为例如重复AAAABBBBCCCC,则可以使用完全不同的方法,即广播矩阵元素而不是向量元素,然后将广播的矩阵元素乘以4个不同{{1 }}一次。

我们甚至可以尝试在不更改数据格式的情况下尝试同时处理多个矩阵,这有助于重用vector_st的负载以提高算术强度。

input_vector