矩阵优化 - 使用内在函数和循环展开时的分段错误

时间:2017-11-20 22:47:40

标签: c segmentation-fault matrix-multiplication intrinsics loop-unrolling

我目前正尝试使用内在函数和循环展开来优化矩阵运算。存在分段错误,我无法弄清楚。这是我改变的代码:

const int UNROLL = 4;

void outer_product(matrix *vec1, matrix *vec2, matrix *dst) {
    assert(vec1->dim.cols == 1 && vec2->dim.cols == 1 && vec1->dim.rows == dst->dim.rows && vec2->dim.rows == dst->dim.cols);
    __m256 tmp[4];
    for (int x = 0; x < UNROLL; x++) {
        tmp[x] = _mm256_setzero_ps();
    } 
    for (int i = 0; i < vec1->dim.rows; i+=UNROLL*8) {
        for (int j = 0; j < vec2->dim.rows; j++) {     
            __m256 row2 = _mm256_broadcast_ss(&vec2->data[j][0]);
            for (int x = 0; x<UNROLL; x++) {
                tmp[x] = _mm256_mul_ps(_mm256_load_ps(&vec1->data[i+x*8][0]), row2); 
                _mm256_store_ps(&dst->data[i+x*8][j], tmp[x]);
            } 
        }
    }
}

void matrix_multiply(matrix *mat1, matrix *mat2, matrix *dst) {
    assert (mat1->dim.cols == mat2->dim.rows && dst->dim.rows == mat1->dim.rows && dst->dim.cols == mat2->dim.cols);
    for (int i = 0; i < mat1->dim.rows; i+=UNROLL*8) {
        for (int j = 0; j < mat2->dim.cols; j++) {
        __m256 tmp[4];
            for (int x = 0; x < UNROLL; x++) {
                tmp[x] = _mm256_setzero_ps();
            } 
            for (int k = 0; k < mat1->dim.cols; k++) {
                __m256 mat2_s = _mm256_broadcast_ss(&mat2->data[k][j]);
                for (int x = 0; x < UNROLL; x++) {
                    tmp[x] = _mm256_add_ps(tmp[x], _mm256_mul_ps(_mm256_load_ps(&mat1->data[i+x*8][k]), mat2_s));
                }
            }
            for (int x = 0; x < UNROLL; x++) {
                _mm256_store_ps(&dst->data[i+x*8][j], tmp[x]);
            }    
        }
    }    
}

编辑: 这是矩阵的结构。我没有修改它。

typedef struct shape {
    int rows;
    int cols;
} shape;

typedef struct matrix {
    shape dim;
    float** data;
} matrix;

编辑: 我试过gdb来找出导致分段错误的行,看起来好像是_mm256_load_ps()。我是否以错误的方式索引到矩阵中,使得它无法从正确的地址加载?对齐记忆的问题是什么?

1 个答案:

答案 0 :(得分:1)

在至少一个地方,你正在进行32字节对齐所需的加载,只有4个字节的步幅。我认为这不是你真正打算做的事情,但是:

for (int k = 0; k < mat1->dim.cols; k++) {
    for (int x = 0; x < UNROLL; x++) {
        ...
        _mm256_load_ps(&mat1->data[i+x*8][k])
     }
 }

_mm256_load_ps加载了8个连续的float s,即它将data[i+x*8][k]加载到data[i+x*8][k+7]我认为你想要data[i+x][k*8]并在最里面的循环中循环k

如果您需要未对齐的加载/存储,请使用_mm256_loadu_ps / _mm256_storeu_ps。但是更喜欢将数据对齐到32B,并填充矩阵的存储布局,以便行跨度是32字节的倍数。 (数组的实际逻辑维度不必与步幅匹配;将每行末尾的填充保留为16或32字节的倍数是很好的。这使得循环更容易编写。)

你甚至没有使用2D数组(你正在使用指向float数组的指针数组),但语法与float A[100][100]的语法相同,即使它的意思是asm非常不同。无论如何,在Fortran 2D数组中,索引是另一种方式,其中递增最左边的索引会将您带到内存中的下一个位置。但是在C中,将左侧索引改为1可以将您带到一个全新的行。 (由float **data的不同元素指向,或者在正确的2D数组中指向一行。当然,由于此混合与使用x*8相结合,您将大步前进8行。 / p>

说到asm,你会得到这个代码非常糟糕的结果,特别是对于gcc,它会为每个向量重新加载4个东西,我想因为它不确定向量存储不会对指针数据进行别名。将事物分配给局部变量以确保编译器可以将它们从循环中提升出来。 (例如const float *mat1dat = mat1->data;。)Clang稍微好一些,但是源中的访问模式本质上是坏的,并且需要针对每个内循环迭代的指针追逐才能到达新行,因为你循环{{1}而不是x。我把它放在Godbolt compiler explorer上。

但是在尝试手动向量化之前,你应该首先优化内存布局。可能值得转置其中一个数组,因此您可以为一个矩阵的行和另一个的列循环连续内存,同时执行行和列的点积来计算结果的一个元素。或者值得在内循环中进行k而不是在前面进行转置(但这是很多内存流量)。但无论你做什么,请确保你没有在内循环中跨越一个矩阵的非连续访问。

您还希望抛弃指针数组并进行手动2D索引(c[Arow,Bcol] += a_value_from_A * b[Arow,Bcol],这样您的数据就在一个连续的块中,而不是单独分配每一行。首先进行此更改,在你花费任何时间手动矢量化之前,似乎最有意义。

gcc或clang with data[row * row_stride + col]应该做一个不自然的自动向量化标量C的工作,特别是如果你使用-O3进行编译。 (手动矢量化完成后可以删除-ffast-math,但在使用自动矢量化进行调整时使用它。)

相关:

您可以在查看缓存阻止之前或之后手动进行矢量化,但是当您这样做时,请参阅Matrix Multiplication with blocks