基于OpenMP并行化数组运算及合并结果的优化与内存问题咨询
我需要加速一个函数:该函数接收含n个元素的复数值数组arr,通过BLAS例程计算对该数组的m次运算的总和,最终替换arr的值。为简化说明,假设运算为不同方阵mats与向量arr的矩阵-向量乘法,再乘以标量数组scalars中的对应标量(实际为更复杂的函数组合)。引入BLAS已使性能提升30-40%,我尝试用OpenMP并行化串行代码,并在搭载M3 Pro芯片的MacBook Pro上进行了性能测试,但测试结果显示小数据量下并行版本性能不如串行,大数据量下虽有提升但仍有优化空间。此外,当p=9(对应n=512,m=262144)时,进程因SIGKILL信号中断。
现咨询:
- 是否存在更优雅的并行化实现方式?
- SIGKILL信号是否由内存耗尽导致?
void sumProducts(double complex** arr, uint64_t n, double complex** mats, double complex* scalars, uint64_t m) { __LAPACK_int N = (__LAPACK_int) n; complex double* tmp = calloc(n, sizeof(complex double)); complex double* tmpSum = calloc(n, sizeof(complex double)); __LAPACK_double_complex beta = 0; for (uint64_t i = 0; i < m; ++i) { __LAPACK_double_complex alpha = 1; cblas_zgemv(CblasRowMajor, CblasNoTrans, N, N, &alpha, mats[i], N, *arr, 1, &beta, tmp, 1 ); alpha = (__LAPACK_double_complex) scalars[i]; cblas_zaxpy(N, &alpha, tmp, 1, tmpSum, 1); } free(tmp); *arr = tmpSum; }
void sumProductsOmp(double complex** arr, uint64_t n, double complex** mats, double complex* scalars, uint64_t m) { __LAPACK_int N = (__LAPACK_int) n; complex double* tmp; complex double* tmpSum = calloc(n, sizeof(complex double)); __LAPACK_double_complex beta = 0; #pragma omp parallel default(none) private(tmp) shared(arr, beta, m, mats, n, N, scalars, tmpSum) { #pragma omp for for (uint64_t i = 0; i < m; ++i) { tmp = calloc(n, sizeof(complex double)); __LAPACK_double_complex alpha = 1; cblas_zgemv(CblasRowMajor, CblasNoTrans, N, N, &alpha, mats[i], N, *arr, 1, &beta, tmp, 1 ); alpha = (__LAPACK_double_complex) scalars[i]; #pragma omp critical(sum) { cblas_zaxpy(N, &alpha, tmp, 1, tmpSum, 1); } free(tmp); } } free(tmp); *arr = tmpSum; }
| n | m | 串行耗时 | OpenMP耗时 |
|---|---|---|---|
| 16 | 256 | 0.000079541 | 0.000554125 |
| 32 | 1024 | 0.001001458 | 0.000519291 |
| 64 | 4096 | 0.013269750 | 0.004418000 |
| 128 | 16384 | 0.194747125 | 0.067422792 |
| 256 | 65536 | 11.373802375 | 8.255758541 |
1. 更优雅的并行化实现方式
当前并行代码存在两个核心性能瓶颈:频繁的内存分配/释放和全局临界区的串行阻塞,同时小数据量下的并行调度开销抵消了并行收益。以下是针对性的优化方案:
优化方向1:避免循环内的内存频繁操作
每个循环迭代都调用calloc和free会产生巨大的内存管理开销。改为在每个线程初始化时分配一次临时缓冲区,循环内复用:
void sumProductsOmpOpt(double complex** arr, uint64_t n, double complex** mats, double complex* scalars, uint64_t m) { __LAPACK_int N = (__LAPACK_int) n; complex double* tmpSum = calloc(n, sizeof(complex double)); __LAPACK_double_complex beta = 0; #pragma omp parallel default(none) shared(arr, beta, m, mats, n, N, scalars, tmpSum) { // 每个线程分配一次临时缓冲区,循环内复用 complex double* tmp = calloc(n, sizeof(complex double)); // 每个线程维护局部求和缓冲区,避免临界区 complex double* localSum = calloc(n, sizeof(complex double)); #pragma omp for schedule(static) for (uint64_t i = 0; i < m; ++i) { __LAPACK_double_complex alpha = 1; cblas_zgemv(CblasRowMajor, CblasNoTrans, N, N, &alpha, mats[i], N, *arr, 1, &beta, tmp, 1 ); alpha = (__LAPACK_double_complex) scalars[i]; // 先累加到线程局部缓冲区,无锁开销 cblas_zaxpy(N, &alpha, tmp, 1, localSum, 1); } // 最后合并所有线程的局部结果到全局缓冲区 #pragma omp critical { cblas_zaxpy(N, &((__LAPACK_double_complex){1.0, 0.0}), localSum, 1, tmpSum, 1); } free(tmp); free(localSum); } free(*arr); *arr = tmpSum; }
优化方向2:用线程局部归约替代全局临界区
通过让每个线程维护独立的localSum,避免每次迭代都进入临界区,只在所有线程完成各自的循环块后合并一次结果,彻底消除串行阻塞瓶颈。
优化方向3:控制并行的启用条件
小数据量下(如n=16、m=256),并行调度的开销远大于并行计算的收益,可通过OpenMP的if子句设置阈值,仅当m足够大时才启用并行:
#pragma omp parallel default(none) ... if(m > 1000)
优化方向4:避免BLAS与OpenMP的嵌套并行
Apple Accelerate或OpenBLAS等BLAS库本身默认会启用多线程,嵌套并行会导致线程竞争,降低效率。可以通过设置环境变量(如export OMP_NUM_THREADS=1)或调用BLAS库的API禁用其内部多线程,让OpenMP统一管理并行资源。
2. SIGKILL信号的原因分析
是的,SIGKILL信号大概率由内存耗尽导致。以n=512、m=262144为例:
- 每个
mats[i]是512×512的复矩阵,每个复元素占16字节(2个double),单个矩阵内存占用为512*512*16 = 4MB。 - 262144个这样的矩阵总内存占用为
262144 * 4MB = 1024GB = 1TB,这远远超过M3 Pro MacBook Pro的最大内存(36GB)。
当进程尝试分配远超系统可用内存的空间时,操作系统会通过SIGKILL强制终止进程以释放资源。
内容的提问来源于stack exchange,提问作者Timo59

