OpenMP 是最简便的共享内存多线程并行接口。区别于 MPI 的显式消息传递,OpenMP 通过编译器指令在串行代码基础上标记可并行区域。对于 NUMA 架构的多核 CPU,正确使用 OpenMP 可以充分释放硬件并行潜力。
基本结构:parallel 与 for
#pragma omp parallel 声明一个并行区域:
#include <omp.h>
#include <stdio.h>
int main() {
#pragma omp parallel
{
int tid = omp_get_thread_num();
int nthreads = omp_get_num_threads();
printf("Hello from thread %d of %d\n", tid, nthreads);
}
return 0;
}
编译:gcc -fopenmp hello.c -o hello。
并行循环是 OpenMP 最常见的使用场景。#pragma omp parallel for 将外层循环自动分发给多个线程:
#pragma omp parallel for
for (int i = 0; i < N; i++) {
c[i] = a[i] + b[i];
}
默认情况下,每个线程独立执行分配到的迭代范围。使用 default(none) 可强制显式声明每个变量的数据属性,避免未预期的数据竞争:
#pragma omp parallel for default(none) shared(a, b, c, N) private(i)
for (i = 0; i < N; i++) {
c[i] = a[i] + b[i];
}
sections 多段任务并行
当几个独立的代码块需要并行执行时,使用 sections:
#pragma omp parallel sections
{
#pragma omp section
{ compute_pressure(grid); }
#pragma omp section
{ compute_temperature(grid); }
#pragma omp section
{ compute_velocity(grid); }
}
三个 section 由不同线程执行,隐式 barrier 等待全部完成。
reduction 子句
并行求和或累乘时,reduction 自动处理线程私有累加器和最终归约:
double sum = 0.0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < N; i++) {
sum += a[i] * b[i];
}
// 所有线程的局部 sum 被自动加总到共享变量 sum
| 操作符 | reduction 用途 |
|---|---|
+ | 求和、点积 |
* | 连乘、概率累积 |
max / min | 极值搜索 |
& / ` | /^` |
&& / ` |
double max_val = -INFINITY;
#pragma omp parallel for reduction(max:max_val)
for (int i = 0; i < N; i++) {
if (a[i] > max_val) max_val = a[i];
}
OpenMP 自动实现树形归约,避免串行累加瓶颈。
schedule 调度策略详解
循环迭代分配到线程粒度直接影响性能,OpenMP 提供四种调度方式:
#pragma omp parallel for schedule(static, 16)
for (int i = 0; i < N; i++) { ... }
| 策略 | 机制 | 适用场景 | 开销 | 负载均衡 |
|---|---|---|---|---|
static | 预先均等分配迭代块 | 各迭代计算量均匀,矩阵计算 | 最低 | 均匀负载时最优 |
dynamic | 线程动态获取小任务块 | 迭代计算量差异大,蒙特卡洛 | 较高 | 好 |
guided | 任务块大小指数递减 | 早期迭代开销大,递归问题 | 中 | 较好 |
runtime | 由环境变量 OMP_SCHEDULE 控制 | 运行时灵活调整 | 依赖外部 | 依赖设置 |
参数 chunk 指定每次分配多少迭代。对于静态负载,增大 chunk 减少调度开销;对于动态负载,小块确保及时负载均衡。
N=64, 4 threads, schedule(static, 8):
Thread 0: [0-7] [8-15]
Thread 1: [16-23] [24-31]
Thread 2: [32-39] [40-47]
Thread 3: [48-55] [56-63]
schedule(dynamic, 4): 线程空闲时动态申请 4 个迭代
调度性能实验对比(100M 次迭代,耗时毫秒):
| 策略 | 均匀负载耗时 | 随机不等量负载耗时 |
|---|---|---|
static | 45ms | 280ms |
static + large chunk | 42ms | 310ms |
dynamic, 1024 | 48ms | 95ms |
guided | 46ms | 78ms |
均匀负载选 static,负载波动选 guided 或动态小 chunk。
同步原语
barrier 显式路障
#pragma omp barrier 阻塞线程直到所有线程到达该点。隐式 barrier 出现在 for、sections、single 区域末尾。
#pragma omp parallel
{
phase_one(); // 阶段一计算
#pragma omp barrier // 确保全部完成后再进入阶段二
phase_two(); // 依赖阶段一结果
}
critical / atomic / ordered
critical:保证代码块串行执行:
double local_sum = 0.0;
#pragma omp parallel for reduction(+:local_sum)
for (...) { local_sum += ...; }
#pragma omp critical
{
global_sum += local_sum;
write_log("thread partial", local_sum);
}
atomic:更轻量的单操作原子化,无锁开销最低:
#pragma omp parallel for
for (int i = 0; i < N; i++) {
if (a[i] > threshold) {
#pragma omp atomic
counter++;
}
}
atomic 仅支持有限的运算符(+, -, *, /, &, |, ^, <<, >>)。
ordered:保持循环迭代的执行顺序,适用于输出依赖场景的调试:
#pragma omp parallel for ordered schedule(static)
for (int i = 0; i < N; i++) {
double result = compute(i);
#pragma omp ordered
{
output_buffer[i] = result;
}
}
避免伪共享(False Sharing)
当多个线程写入同一缓存行(通常 64B)的不同位置时,缓存一致性协议导致性能暴跌:
// 错误:所有 counter 可能落在同一缓存行
int counters[64]; // 线程 i 写 counters[i]
// 正确:padding 确保每个 counter 独占缓存行
struct padded_counter {
int val;
char padding[60]; // 64 - sizeof(int)
} __attribute__((aligned(64)));
struct padded_counter counters[64];
task 任务并行
OpenMP 3.0 引入的 task 允许递归和动态划分不可预测的并行性:
void fib_task(int n, int *out) {
if (n < 2) {
*out = n;
return;
}
int x, y;
#pragma omp task shared(x)
fib_task(n - 1, &x);
#pragma omp task shared(y)
fib_task(n - 2, &y);
#pragma omp taskwait
*out = x + y;
}
int main() {
int result;
#pragma omp parallel
{
#pragma omp single
fib_task(40, &result);
}
printf("fib(40) = %d\n", result);
}
关键指令说明:
| 指令 | 作用 |
|---|---|
task | 创建异步任务,可由任意线程执行 |
taskwait | 等待当前任务的所有子任务完成 |
single | 仅一个线程执行块内代码,隐式 barrier |
master | 仅主线程执行,无隐式同步 |
task 模型特别适合图遍历、n-Queens、稀疏矩阵分解等递归分支结构。
环境变量速查
export OMP_NUM_THREADS=64 # 线程数
export OMP_PLACES=cores # 绑定到物理核心
export OMP_PROC_BIND=close # 线程在相邻核心上分布
export OMP_DYNAMIC=false # 禁止运行时动态调整线程数
export OMP_NESTED=true # 允许并行区域嵌套
export OMP_MAX_ACTIVE_LEVELS=2 # 最大嵌套深度
线程绑定对 NUMA 架构至关重要。OMP_PROC_BIND=spread 将线程均匀分散到各 NUMA 节点以最大化聚合带宽;close 则将线程集中在最小核心集以提高缓存利用率。
// 查看运行时绑定信息
#pragma omp parallel
{
int cpuid = sched_getcpu();
printf("Thread %d on CPU %d\n", omp_get_thread_num(), cpuid);
}
OpenMP 以极低的侵入性成本和成熟的编译器支持,成为多核 CPU 并发的首选工具。掌握 reduction、schedule 和 task 三大核心能力,足以应对绝大多数 HPC 和 AI 推理中的 CPU 并行需求。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。