You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过寄存器数据复用加速矩阵向量间Pearson相关系数计算

寄存器复用优化Pearson相关系数计算遇到性能问题

背景

我正在学习并行编程课程,核心知识点是处理器寄存器内数据复用技术(扩展至缓存层级)——即在从内存加载新数据前完成尽可能多的计算,课程使用C++语言。

我尝试将该技术应用于以下问题:给定包含向量$x_i$的矩阵$M$,计算矩阵中每对向量间的Pearson相关系数,结果存储至矩阵$R$,因系数对称仅需计算上三角部分。

朴素实现版本

最初采用双层循环遍历矩阵行,先对矩阵预处理,利用向量类型并行执行乘法与求和。计算系数需获取向量点积及元素和,代码如下:

#include <cmath>
#include <vector>

using namespace std;

#define PARTITION_SIZE 8
#define INIT_PARTITION {.0,.0,.0,.0,.0,.0,.0,.0}

typedef double double4_t __attribute__ ((vector_size (PARTITION_SIZE * sizeof(double))));

void correlate(int ny, int nx, const float *data, float *result) {

    int partitions_per_row = nx % PARTITION_SIZE != 0 ? nx/PARTITION_SIZE + 1 : nx/PARTITION_SIZE;

    vector<double4_t> processed_data((partitions_per_row) * ny);

    /* Load data into vectorized format for easier processing */
    // Traverse rows
    for(int i = 0; i < ny; i++){
        // Traverse partitions in row
        for(int j = 0; j < partitions_per_row; j++){
            // Traverse elements inside a partition
            for (int k = 0; k < PARTITION_SIZE; ++k) {
                int old_x_index = j * PARTITION_SIZE + k;
                // If the element is inside the matrix, load it, otherwise load 0 for padding
                // This way we can have an integer number of partitions per row
                processed_data[i * partitions_per_row + j][k] = old_x_index < nx ? (data[i * nx + old_x_index] ): 0.0;
            }
        }
    }

    /* Calculate Pearson's coefficient between each vector of the matrix */
    for(int i = 0; i < ny; i++){
        double4_t x_partition;
        double4_t y_partition;  

        for(int j = i; j < ny; j++){
            double4_t sum_of_x_partition = INIT_PARTITION;
            double4_t sum_of_y_partition = INIT_PARTITION;
            double4_t sum_of_x_squared_partition = INIT_PARTITION;
            double4_t sum_of_y_squared_partition = INIT_PARTITION;
            double4_t sum_of_x_times_y_partition = INIT_PARTITION;

            int x_row_index = i * partitions_per_row;
            int y_row_index = j * partitions_per_row;
            
            // Traverse partitions in row
            for(int k = 0; k < partitions_per_row; k++){
                x_partition = processed_data[x_row_index + k];
                y_partition = processed_data[y_row_index + k];

                // Calculate the sums of each partition
                sum_of_x_partition += x_partition;
                sum_of_y_partition += y_partition;
                sum_of_x_squared_partition += x_partition * x_partition;
                sum_of_y_squared_partition += y_partition * y_partition;
                sum_of_x_times_y_partition += x_partition * y_partition;
            }

            // Agregate the different sums
            for (int k = 1; k < PARTITION_SIZE; ++k) {
                sum_of_x_partition[0] += sum_of_x_partition[k];
                sum_of_y_partition[0] += sum_of_y_partition[k];
                sum_of_x_squared_partition[0] += sum_of_x_squared_partition[k];
                sum_of_y_squared_partition[0] += sum_of_y_squared_partition[k];
                sum_of_x_times_y_partition[0] += sum_of_x_times_y_partition[k];
            }

            // Calculate the correlation
            double numerator = nx * sum_of_x_times_y_partition[0] - sum_of_x_partition[0] * sum_of_y_partition[0];
            double denominator = sqrt((nx * sum_of_x_squared_partition[0] - sum_of_x_partition[0] * sum_of_x_partition[0]) * 
                                                        (nx * sum_of_y_squared_partition[0] - sum_of_y_partition[0] * sum_of_y_partition[0]));

            double correlation = numerator / denominator;

            result[i * ny + j] = correlation;
        }
    }
}

寄存器复用的首次实现

我发现常规遍历难以复用数据,故先转置矩阵,使每行包含各向量的对应元素,再转换为向量格式以利用处理器向量操作。思路是通过整向量乘法结合元素置换获取所有元素组合,减少运算量,同时重复遍历同一行提升读写效率。使用_mm256_permute4x64_pd与_mm256_permute_pd置换向量,乘积存入temporal_mults,向量和存入sums_vector。

但该实现虽可运行,性能却远低于朴素版本,无法确定是实现缺陷还是技术理解有误,特附上代码:

#include <cmath>
#include <vector>
#include <x86intrin.h>

using namespace std;

// The registers are 256 bits long, so we can fit 4 doubles in one register.
#define PARTITION_SIZE 4
#define INIT_PARTITION {.0,.0,.0,.0}

typedef double double4_t __attribute__ ((vector_size (PARTITION_SIZE * sizeof(double))));

static inline double4_t swap2(double4_t x) { return _mm256_permute4x64_pd(x, 0b01001110); }
static inline double4_t swap1(double4_t x) { return _mm256_permute_pd(x, 0b0101); }


/* Load data into vectorized format for easier processing.
   It is a bit hard to wrap your head around the indeces,
   but basically we are creating the transpose of the matrix, 
   vectorizing the data and adding padding at the same time.
*/
vector<double4_t> optimizeMatrix(int nx, int ny, int partitions_per_column, const float *data){
    vector<double4_t> processed_data(nx*partitions_per_column);
    // Traverse columns
    for(int i = 0; i < nx; i++){
        // Traverse rows
        for(int j = 0; j < partitions_per_column; j++){
            int processed_data_column = i * partitions_per_column;
            int processed_vector_index = processed_data_column + j;

            for (int k = 0; k < PARTITION_SIZE; ++k) {
                int old_column = j * PARTITION_SIZE + k;
                processed_data[processed_vector_index][k] = 
                    old_column < ny ? data[old_column * nx + i] : .0; 
            }
        }
    }
    return processed_data;
}

void computeRowMultiplications(
        int ny,
        int processed_data_row,
        int partitions_per_column,
        vector<double4_t> &processed_data,
        vector<double4_t> &sums_vector,
        vector<double> &temporal_mults){

    for(int partition_a = 0; partition_a < partitions_per_column; partition_a++){
        double4_t a000 = processed_data[processed_data_row + partition_a];
        double4_t a001 = swap1(a000);

        sums_vector[partition_a] += a000;

        int starting_ele_index = partition_a * PARTITION_SIZE; // Translate partition number to vector index
        int result_row_1 = starting_ele_index * ny; // Calculate result row for first element from vector index
        
        for(int partition_b = partition_a; partition_b < partitions_per_column; partition_b++){
            double4_t b000 = processed_data[processed_data_row + partition_b];
            double4_t b001 = swap1(b000);
            double4_t b010 = swap2(b000);

            double4_t mult_vector[PARTITION_SIZE];
            mult_vector[0] = a000 * b000;
            mult_vector[1] = a000 * b001;
            mult_vector[2] = a000 * b010;
            mult_vector[3] = swap1(a001 * b010);

            short contador_2 = -1, contador_4 = -2;
            for(int perm = 0, perm_opposite = PARTITION_SIZE - 1; perm < PARTITION_SIZE; perm++, perm_opposite--){
                contador_2 *= -1;
                if (perm % 2 == 0) {
                    contador_4 *= -1;
                }
                temporal_mults[result_row_1 + partition_b * PARTITION_SIZE + perm] += mult_vector[perm][0];
                temporal_mults[result_row_1 + ny + partition_b * PARTITION_SIZE + perm] += mult_vector[perm + contador_2][1];
                temporal_mults[result_row_1 + ny + ny + partition_b * PARTITION_SIZE + perm] += mult_vector[perm + contador_4][2];
                temporal_mults[result_row_1 + ny + ny + ny + partition_b * PARTITION_SIZE + perm] += mult_vector[perm_opposite][3];
            }
        }
    }
}

void correlate(int ny, int nx, const float *data, float *result) {

    int partitions_per_column = ny % PARTITION_SIZE != 0 ? ny/PARTITION_SIZE + 1 : ny/PARTITION_SIZE;

    vector<double4_t> processed_data;
    vector<double> temporal_mults( pow(partitions_per_column * PARTITION_SIZE, 2), 0.0);

    vector<double4_t> sums_vector(partitions_per_column);
    for (auto& vec : sums_vector) {
        vec[0] = 0.0;
        vec[1] = 0.0;
        vec[2] = 0.0;
        vec[3] = 0.0;
    }
 
    processed_data = optimizeMatrix(nx, ny, partitions_per_column, data);

    /* Calculate the multiplication between vectors and store them in temporal_muls,
       and the element sum of each vector and store them in sums_vectors */
    for(int i = 0; i < nx; i++){
        int current_row = i*partitions_per_column;
        computeRowMultiplications(
            ny,
            current_row, 
            partitions_per_column, 
            processed_data,
            sums_vector,
            temporal_mults);

    }

    /* Use the values computed above to calculate the correlations between vectors,
       and store them in results. */
    for(int i = 0; i < ny; i++){
        int current_row = i * ny;

        double sum_i = sums_vector[i/PARTITION_SIZE][i % PARTITION_SIZE];
        double squared_sum_i = temporal_mults[current_row + i];
        for(int j = ny-1; j >= i; j--){
            double sum_j = sums_vector[j/PARTITION_SIZE][j % PARTITION_SIZE];
            double squared_sum_j = temporal_mults[j*ny + j];

            int current_cell = current_row + j;
            double sum_of_i_times_j = temporal_mults[current_cell];

            double numerator = nx * sum_of_i_times_j - sum_i * sum_j;
            double denominator = sqrt((nx * squared_sum_i - sum_i * sum_i) * 
                                      (nx * squared_sum_j - sum_j * sum_j));

            result[current_cell] = numerator / denominator;
        }
    }
}

求助

作为并行高效编程新手,我确信存在诸多未察觉的问题,恳请各位提供帮助。若问题表述存在不当之处,也请指出。


内容的提问来源于stack exchange,提问作者Lucas Piorno

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 06:08:11