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

为何printf调用能提升OpenMP稀疏矩阵多向量乘法的执行效率?

预处理函数中的printf调用大幅提升OpenMP SpMM性能的问题

我在基于OpenMP实现稀疏矩阵-多向量乘法(SpMM)时,发现一个诡异现象:在未并行化的预处理函数的循环中加入若干printf调试调用后,后续并行乘积计算的GFLOPS值从约2大幅提升至10。我尝试将计时方式从clock_gettime()改为omp_get_wtime(),结果几乎一致。

核心主函数代码

int main() {

    // some stuff

    // 串行乘积计算
    clock_gettime(CLOCK_MONOTONIC, &t1);
    serial_product(a, x, k, y_s);
    clock_gettime(CLOCK_MONOTONIC, &t2);

    gflops_s = flop / ((t2.tv_sec - t1.tv_sec) * 1.e9 + (t2.tv_nsec - t1.tv_nsec));

    pre_processing(...) // 这里是用于负载均衡的预处理函数

    clock_gettime(CLOCK_MONOTONIC, &t1);
    openmp_spmm(a, rows_idx, num_threads, x, k, y_p);
    clock_gettime(CLOCK_MONOTONIC, &t2);

    gflops_p = flop / ((t2.tv_sec - t1.tv_sec) * 1.e9 + (t2.tv_nsec - t1.tv_nsec));

    // some other stuff
}

如上代码所示,若不在预处理函数的循环中加入调试用的printf,gflops_p约为2 GFLOPS;加入后则跃升至10 GFLOPS,而这些printf与OpenMP并无直接关联。

预处理函数代码

void pre_processing(int threads, int tot_nz, const int* irp, int tot_rows, int* rows_idx){
    int j, nz, nz_prev = 0, nz_curr, start_row = 0, r_prev, r_curr = 0;

    for (int i = 0; i < threads; i++) {
        rows_idx[i] = start_row; 

        printf("."); // 为什么这个打印能大幅加速OpenMP乘积计算?

        nz_curr = ((i + 1) * tot_nz) / threads;
        nz = nz_curr - nz_prev;
        nz_prev = nz_curr;

        for (j = start_row; j < tot_rows; j++) {
            r_curr += irp[j + 1] - irp[j]; 

            if (r_curr < nz) { 
                r_prev = r_curr; 
            } else {
                start_row = ((r_curr - nz) < (nz - r_prev)) ? j + 1 : j;
                break;
            }
        }

        r_curr = 0;
    }
    rows_idx[threads] = tot_rows;
    printf("\n");
}

我完全不清楚该现象的原因,猜测可能与stdout刷新、时钟周期或CPU利用率有关?

运行环境(EDIT 1)

程序运行于搭载CentOS Stream 8(Linux内核4.18.0-448.el8.x86_64)的服务器,CPU参数如下:

Architecture:        x86_64
CPU op-mode(s):      32-bit, 64-bit
Byte Order:          Little Endian
CPU(s):              40
On-line CPU(s) list: 0-39
Thread(s) per core:  2
Core(s) per socket:  10
Socket(s):           2
NUMA node(s):        2
Vendor ID:           GenuineIntel
CPU family:          6
Model:               85
Model name:          Intel(R) Xeon(R) Silver 4210 CPU @ 2.20GHz
Stepping:            7
CPU MHz:             2200.000
CPU max MHz:         3200.0000
CPU min MHz:         1000.0000
BogoMIPS:            4400.00
Virtualization:      VT-x
L1d cache:           32K
L1i cache:           32K
L2 cache:            1024K
L3 cache:            14080K
NUMA node0 CPU(s):   0-9,20-29
NUMA node1 CPU(s):   10-19,30-39

SpMM核心实现与猜测(EDIT 2)

我猜测该现象可能与SpMM例程中使用的AVX指令有关,有观点指出问题可能源于编译器的向量化与内联优化。这是我首次使用这类指令,对其执行影响理解不足。SpMM代码如下:

void spmm_csr_64(CSR *mat, const int* rows_load, int threads, const Type* x, int k, Type* y){

    int *irp = mat->IRP;
    int *ja = mat->JA;
    Type *as = mat->AS;

    const __m256i scale = _mm256_set1_epi32(k);

    #pragma omp parallel for num_threads(threads) shared(threads, rows_load, irp, k, as, ja, x, y, scale) default(none)
    for (int tid = 0; tid < threads; tid++) {   // 按线程ID并行化
        // 私有参数
        int j, z, iter, lim, r_y, r_x;
        Type val;
        __m256i cols;
        __m512d vals, x_r;

        __m512d t[k];
        #pragma omp unroll partial
        for (z = 0; z < k; z++) {
            t[z] = _mm512_setzero_pd(); // 初始化t向量
        }

        for (int i = rows_load[tid]; i < rows_load[tid + 1]; i++) { // 线程处理分配到的行
            j = irp[i];
            lim = (irp[i+1] - j) / 8;

            for (iter = 0; iter < lim; iter++) {

                vals = _mm512_loadu_pd(&as[j]);                     // 加载8个64位元素
                cols = _mm256_loadu_si256((__m256i*)&ja[j]);        // 加载8个32位列索引
                cols = _mm256_mullo_epi32(cols, scale);             // 将列索引缩放至x的首列位置
                x_r = _mm512_i32gather_pd(cols, x, sizeof(Type));   // 根据列索引从x中收集元素
                t[0] = _mm512_fmadd_pd(vals, x_r, t[0]);            // 执行融合乘加操作

                for (z = 1; z < k; z++) {
                    cols = _mm256_add_epi32(cols, _MM8_1);
                    x_r = _mm512_i32gather_pd(cols, x, sizeof(Type));      // 根据列索引从x中收集元素
                    t[z] = _mm512_fmadd_pd(vals, x_r, t[z]);    // 执行融合乘加操作
                }

                j += 8;
            }

            r_y = i * k;

            // 处理剩余元素(当元素数不是8的倍数时)
            for (; j < irp[i+1]; j++) {
                val = as[j];
                r_x = ja[j] * k;

                #pragma omp unroll partial
                for (z = 0; z < k; z++) {
                    y[r_y + z] += val * x[r_x + z];
                }
            }

            // 对t中的所有64位元素求和归约
            #pragma omp unroll partial
            for (z = 0; z < k; z++) {
                y[r_y + z] += _mm512_reduce_add_pd(t[z]);
                t[z] = _mm512_setzero_pd();
            }
        }
    }
}

我使用Type作为double的占位符,便于后续切换为float,目前使用双精度。同时欢迎提供代码优化建议。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 20:19:52