为何使用OpenMP的C代码比单线程版本运行更慢?
问题:OpenMP并行蒙特卡洛计算π比单线程慢的原因及解决办法
我是多线程开发新手,为练习尝试用蒙特卡洛采样法计算圆周率π,但测试发现使用-fopenmp编译的代码运行速度远慢于单线程版本。以下是并行化代码段:
#pragma omp parallel for default(none) shared(success, random) for(i = 0; i < n; i++){ unsigned int temp; fread(&temp, sizeof(unsigned int), 1, random); double xval = (double)temp / UINT_MAX; fread(&temp, sizeof(unsigned int), 1, random); double yval = (double)temp / UINT_MAX; if((xval*xval + yval*yval) < 1){ #pragma omp atomic ++success; } }
我已尽量减少共享变量,也尝试调整不同schedule值消除伪共享,但效果不明显,并行版本仍远慢于单线程。通过gprof分析发现程序在fread函数上耗时较多,尝试用rand()替代读取/dev/random后速度反而更慢,同时锁函数也消耗大量时间。我疑惑原子操作是否是性能瓶颈?如果是,有什么更好的方式递增success?我也尝试过对success使用reduction,但收效甚微。
以下是gprof输出结果:
% cumulative self self total time seconds seconds calls ms/call ms/call name 37.88 0.63 0.63 __lll_lock_wait_private 26.67 1.07 0.44 fread 23.64 1.46 0.39 __lll_lock_wake_private 4.85 1.54 0.08 3 26.67 26.67 main 1.82 1.57 0.03 _IO_file_xsgetn 1.82 1.60 0.03 _IO_sgetn 1.82 1.63 0.03 gomp_team_barrier_wait_end 0.61 1.64 0.01 fputs 0.61 1.65 0.01 read 0.30 1.65 0.01 __libc_cleanup_pop_restore
解决方案
核心瓶颈分析
从gprof结果和代码能定位两个致命性能问题:
- 共享文件流的锁竞争:
__lll_lock_wait_private和__lll_lock_wake_private合计占比超60%,所有线程共享同一个random文件流,fread内部的线程安全锁导致大量线程等待,这是最大的性能杀手。 - 低效的随机源:
/dev/random会阻塞等待系统熵池,速度极慢;rand()是全局函数,内部同样有锁,替换后锁竞争更严重。 - 原子操作确实有开销,但相比锁竞争,它不是当前的核心瓶颈——你之前用Reduction收效甚微,大概率是用法错误。
针对性修复方案
1. 给每个线程分配独立的随机源
方案A:用线程局部伪随机数生成器(rand_r)
rand_r是线程安全的,每个线程维护自己的种子,无全局锁。结合Reduction彻底消除原子操作开销:
#include <omp.h> #include <stdlib.h> #include <stdint.h> #include <time.h> // ... int success = 0; #pragma omp parallel reduction(+:success) { // 每个线程用唯一种子:线程ID+时间戳,避免重复序列 unsigned int seed = omp_get_thread_num() + (unsigned int)time(NULL); int thread_count = omp_get_num_threads(); int thread_id = omp_get_thread_num(); // 手动划分迭代区间,减少调度开销 for (int i = thread_id; i < n; i += thread_count) { unsigned int temp = rand_r(&seed); double xval = (double)temp / UINT_MAX; temp = rand_r(&seed); double yval = (double)temp / UINT_MAX; if (xval*xval + yval*yval < 1) { success++; // Reduction自动处理私有副本,无需原子操作 } } }
方案B:每个线程独立打开/dev/urandom
如果必须用真随机数,不要共享文件流,改用/dev/urandom(无阻塞,速度远快于/dev/random):
#include <omp.h> #include <stdio.h> #include <stdint.h> // ... int success = 0; #pragma omp parallel reduction(+:success) { FILE* random = fopen("/dev/urandom", "r"); if (!random) { perror("Failed to open /dev/urandom"); return 1; } int thread_count = omp_get_num_threads(); int thread_id = omp_get_thread_num(); for (int i = thread_id; i < n; i += thread_count) { unsigned int temp; fread(&temp, sizeof(unsigned int), 1, random); double xval = (double)temp / UINT_MAX; fread(&temp, sizeof(unsigned int), 1, random); double yval = (double)temp / UINT_MAX; if (xval*xval + yval*yval < 1) { success++; } } fclose(random); }
2. 正确使用Reduction
你之前用Reduction无效,大概率是没有在并行指令中正确声明。正确写法是将success声明为Reduction变量,而非shared:
// 正确的parallel for + Reduction写法 #pragma omp parallel for reduction(+:success) default(none) shared(n) for (int i = 0; i < n; i++) { // ... 生成xval、yval ... if (xval*xval + yval*yval < 1) { success++; // 直接操作私有副本,无原子开销 } }
3. 额外优化技巧
- 整数运算替代浮点:避免浮点除法和乘法的开销,将随机数直接用整数判断(注意用64位整数防止溢出):
uint64_t x = temp; uint64_t y = temp2; if (x*x + y*y <= (uint64_t)UINT_MAX * UINT_MAX) { success++; } - 设置合适的线程数:通过
export OMP_NUM_THREADS=你的CPU核心数调整线程数,避免线程过多导致上下文切换。
效果验证
修改后,gprof中的锁等待时间会大幅下降,并行效率显著提升,运行速度会远超单线程版本。
内容的提问来源于stack exchange,提问作者Lentemern
相关产品推荐
相关产品推荐

