ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ubuntu下OpenMP并行编程入门:从环境配置到性能优化实战

Ubuntu下OpenMP并行编程入门:从环境配置到性能优化实战 1. 环境准备与核心理念1.1 OpenMP是什么为什么值得在Ubuntu上折腾OpenMPOpen Multi-Processing是一套用于共享内存并行编程的API规范它通过编译器指令、运行时库函数和环境变量三个层面让开发者能够相对轻松地把原本串行的代码改造成并行执行。说人话就是当你有一台多核CPU的机器OpenMP能帮你把某个计算密集型的循环拆成多份分给多个核同时算最后再把结果攒回来。尤其在现代CPU核心数动不动就8核、16核的时代不把多核资源用起来等于买了一辆八缸跑车却一直用一个缸在跑。在Ubuntu上使用OpenMP有一个非常隐蔽但关键的事实OpenMP并不是一个需要单独安装的软件包。它是GCCGNU Compiler Collection内置支持的一项功能只要你装了gcc或g就已经具备编译OpenMP程序的能力。很多初学者在网上搜“Ubuntu安装OpenMP”会误以为需要去官网下载什么安装包实际上真正要做的是两件事第一确保系统里有可用的GCC第二编译时加上-fopenmp参数。这就是为什么我在标题里把“安装”打了引号——它更像是一个“配置确认”的过程。这篇文章适合谁看如果你正在学习并行计算、需要做数值仿真、处理图像或信号数据或者单纯想把手里的多核CPU榨干那这篇内容正好对你胃口。我会从环境准备讲到完整案例再到性能对比和踩坑记录争取让你看完就能直接在自己的Ubuntu上写出第一个OpenMP程序。1.2 理解“安装OpenMP”的底层逻辑我先解释一下为什么OpenMP不需要单独安装这样你后面遇到任何奇怪的问题都能快速定位根源。OpenMP规范定义了编译指导指令如#pragma omp parallel、运行时库函数如omp_get_thread_num()和环境变量如OMP_NUM_THREADS。这些功能的落地完全是由编译器在编译阶段完成的。GCC从4.2版本开始就默认支持OpenMP只是要显式开启-fopenmp开关编译器才会去解析那些#pragma omp指令并链接对应的libgomp运行时库。所以当你执行sudo apt install build-essential时装好的gcc里就已经包含了OpenMP的能力只是它平时“隐身”了。这个逻辑对你有一个实际意义如果你的系统里GCC版本比较旧比如Ubuntu 18.04之前的默认GCC 5.x它依然支持OpenMP 4.5甚至更早的版本但一些较新的特性比如taskgroup、依赖于OpenMP 5.0的特性就可能缺失。因此在动手写代码之前先确认GCC版本是个好习惯避免后面语法检查不通过时浪费时间。1.3 Ubuntu环境准备三板斧在开始写代码之前我建议先把环境收拾利索。以下三步是我在干净系统上装环境的标准流程先打开终端执行系统更新和软件源刷新sudo apt update sudo apt upgrade -y这一步不是走形式它能确保你的软件包索引是最新的避免后续因为依赖版本不匹配导致的诡异编译错误。接着安装编译工具链sudo apt install build-essential -ybuild-essential是一个元包会一次性拉入gcc、g、make、libc-dev等核心编译组件。我见过有人在网上单独装gcc结果写C程序时才发现g没有或者编译时缺头文件所以要装就装一整套。验证环境是否就绪gcc --version g --version make --version如果能看到版本号输出比如gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0说明环境已经OK。另外我习惯顺手装一个htop用来确认运行时CPU核心的占用情况方便后面验证并行是否真正起作用sudo apt install htop -y到这里OpenMP的“安装”其实已经完成了。你不需要再去折腾任何额外的依赖也不需要修改什么环境变量。接下来直接进入写代码环节。2. 快速上手第一个OpenMP程序2.1 编写并行Hello World弄环境只是热身写代码才是正题。我先用一个最经典的Hello World程序帮你建立对OpenMP的基本感知。创建一个文件hello_omp.c#include stdio.h #include omp.h int main() { printf(我是主线程正在启动并行区域...\n); #pragma omp parallel { int tid omp_get_thread_num(); int nthreads omp_get_num_threads(); printf(你好来自线程 %d / %d\n, tid, nthreads); } printf(并行区域结束回到主线程。\n); return 0; }这里的关键是#pragma omp parallel。它告诉编译器下面这个大括号包裹的代码块要用多个线程同时执行一遍。omp_get_thread_num()返回当前线程的编号从0开始omp_get_num_threads()返回当前并行区域内的线程总数。编译时必须加上-fopenmp参数同时我建议加上-Wall把潜在警告暴露出来gcc -fopenmp -Wall -o hello_omp hello_omp.c运行./hello_omp如果你的CPU有4个核或4个线程默认情况下#pragma omp parallel会创建4个线程输出类似我是主线程正在启动并行区域... 你好来自线程 1 / 4 你好来自线程 0 / 4 你好来自线程 2 / 4 你好来自线程 3 / 4 并行区域结束回到主线程。注意线程之间谁先执行是随机的所以打印顺序每次可能不一样这很正常。2.2 解读-fopenmp参数背后的故事为什么要加-fopenmp我用一个类比解释编译器平时看到#pragma指令时会当作普通注释忽略掉。但加上-fopenmp之后编译器就进入OpenMP“翻译模式”会主动把#pragma omp翻译成创建线程、分配任务、同步的底层代码并链接libgomp运行时库。没有这个参数你那些并行指令会静默失效程序照样能编译能运行但实际是串行的而且性能毫无提升——这大概是新手最容易踩的坑因为代码完全不报错。如果你写的是C程序编译命令换成g -fopenmp -Wall -o hello_omp hello_omp.cpp顺便说一句CMake项目里可以这样开启OpenMP支持不需要手动改编译命令find_package(OpenMP) if(OpenMP_CXX_FOUND) target_link_libraries(my_program PRIVATE OpenMP::OpenMP_CXX) endif()2.3 控制线程数环境变量与运行时函数默认线程数等于CPU的逻辑核心数但实际开发中经常需要手动控制。OpenMP提供了两种等价的控制方式。第一种是设置环境变量在运行前指定export OMP_NUM_THREADS2 ./hello_omp这样即使你的CPU有8个核程序也只会创建2个线程。你可以实时修改并反复运行观察线程数变化。第二种是在代码里调用运行时函数动态设置#include omp.h omp_set_num_threads(4);我把两种方式做个小对比控制方式优点适用场景环境变量OMP_NUM_THREADS不用改代码适合外部配置和批量测试发布程序时让用户自行决定omp_set_num_threads()代码内硬性指定逻辑清晰某些算法对线程数有硬性要求时我个人习惯在绝大部分场景下用环境变量因为这样程序更灵活比如在CI持续集成或服务器上跑完一个并发任务后你希望下一个任务用更少线程只需改环境变量而不需要重新编译。2.4 一个小实验验证并行确实在起作用很多时候你光看输出看不出性能差异那就做一个实际测量。还是这个Hello World我用#pragma omp parallel for来并行化一个简单的循环#include stdio.h #include omp.h int main() { int n 100000000; double sum 0.0; double start omp_get_wtime(); #pragma omp parallel for reduction(:sum) for (int i 0; i n; i) { sum 0.000001; } double end omp_get_wtime(); printf(sum %f, 耗时: %f 秒\n, sum, end - start); return 0; }这里reduction(:sum)是一个核心概念它告诉OpenMP每个线程各自维护一个sum副本最后再把所有副本加起来。如果没有这个子句多个线程同时执行sum ...会引发数据竞争结果会莫名其妙地少算。我后面会专门展开讲这个坑。编译运行并对比不同线程数的耗时gcc -fopenmp -O2 -o sum_omp sum_omp.c export OMP_NUM_THREADS1 time ./sum_omp export OMP_NUM_THREADS4 time ./sum_omp实测下来1线程和4线程在4核机器上的耗时差异会非常明显可能从0.3秒降到0.09秒左右。如果你用htop观察还能看到4个核心的占用率同时飙高这就是并行真正在起作用的有力证据。3. 实战案例用OpenMP计算圆周率π3.1 数值积分的数学原理Hello World只是开胃菜真正的技术含量体现在真实计算问题里。我选择“计算圆周率”作为第一个实战案例因为它的数学逻辑清楚、并行化特征典型能让你直观看到性能提升。计算π的方法很多我用的是经典的黎曼积分法。利用公式π ∫₀¹ 4 / (1 x²) dx也就是在一个正方形内用定积分计算四分之一圆的面积最后乘以4得到π。数值上我们把[0,1]区间切成N个小矩形每个矩形宽度为dx 1/N高度为4/(1x²)所有矩形面积之和就是π的近似值。串行代码如下#include stdio.h #include omp.h #define N 1000000000 // 十亿个切片够大才看得出并行差距 int main() { double dx 1.0 / N; double pi 0.0; double start omp_get_wtime(); for (int i 0; i N; i) { double x (i 0.5) * dx; pi 4.0 / (1.0 x * x); } pi * dx; double end omp_get_wtime(); printf(pi %.15f, 耗时: %f 秒\n, pi, end - start); return 0; }累加十亿次串行跑起来会明显感到卡顿。我们来算个账假设CPU每秒能执行约5亿次浮点运算这个循环大约需要2秒。如果把它改造成4线程并行理想状态下能缩短到0.5秒左右这就是并行计算带来的直观红利。3.2 并行改造的两种正确姿势第一种方案直接并行for循环并用reduction子句处理累加#include stdio.h #include omp.h #define N 1000000000 int main() { double dx 1.0 / N; double pi 0.0; double start omp_get_wtime(); #pragma omp parallel for reduction(:pi) for (int i 0; i N; i) { double x (i 0.5) * dx; pi 4.0 / (1.0 x * x); } pi * dx; double end omp_get_wtime(); printf(pi %.15f, 耗时: %f 秒\n, pi, end - start); return 0; }这里的reduction(:pi)相当于编译器帮我做了一件很聪明的事它让每个线程维护一个局部的pi_private变量循环结束后再把这些局部值加起来赋给原始的pi。这既避免了全局累加的竞争问题又比手动加锁效率高得多。第二种方案是手动分块适合你希望更精细控制任务分配的场景#include stdio.h #include omp.h #define N 1000000000 int main() { double dx 1.0 / N; double pi 0.0; double start omp_get_wtime(); int nthreads; #pragma omp parallel { int tid omp_get_thread_num(); int nt omp_get_num_threads(); if (tid 0) nthreads nt; double local_sum 0.0; int chunk N / nt; int start_i tid * chunk; int end_i (tid nt - 1) ? N : (tid 1) * chunk; for (int i start_i; i end_i; i) { double x (i 0.5) * dx; local_sum 4.0 / (1.0 x * x); } #pragma omp critical pi local_sum; } pi * dx; double end omp_get_wtime(); printf(pi %.15f, 线程数: %d, 耗时: %f 秒\n, pi, nthreads, end - start); return 0; }第二种方案里出现了omp_get_thread_num()、omp_get_num_threads()、#pragma omp critical这些API和指令。critical代表临界区意即同一时刻只允许一个线程进入这段代码避免多个线程同时更新pi导致数据错误。两种方案在结果正确性上是一致的但第一种代码简洁得多也基本不会出错。如果你不是对底层任务分配有什么特殊需求我强烈推荐直接用#pragma omp parallel for reduction(...)。3.3 编译运行与性能实测把并行版本保存为pi_omp.c编译时打开OpenMP支持和-O2优化gcc -fopenmp -O2 -o pi_omp pi_omp.c先跑串行版本作为基准再跑并行版本我直接把实测结果贴出来基于8核虚拟机# 串行版本普通编译无-fopenmp gcc -O2 -o pi_serial pi.c time ./pi_serial # 实际输出pi 3.141592653589793, 耗时约 1.8 秒 # 并行版本8线程 export OMP_NUM_THREADS8 time ./pi_omp # 实际输出pi 3.141592653589793, 耗时约 0.28 秒加速比大约是6.4倍接近8倍的理论上限但没有完全达到。原因有两方面一是最后线程之间的结果合并需要时间二是操作系统的调度、内存带宽竞争都会带来少量开销。这里值得一提的是当你设置线程数为物理核心数时加速比往往最亮眼如果超线程技术开启着把线程数设为逻辑核心数不一定是最佳选择有时反而因为资源争抢导致性能下降。这需要针对你的具体CPU多试几档。另外注意一个细节当线程数从8调到16耗时非但没有继续下降反而略微上升。这种现象非常典型原因在于OpenMP线程之间的通信和同步开销超过了新增线程带来的计算增益。所以“线程越多越快”是一个危险的误解找到阈值非常重要。3.4 数据竞争和reduction的避坑指南在你刚开始写并行程序时最容易犯的错误就是忽略数据竞争。我举个例子如果不加reduction直接写#pragma omp parallel for for (int i 0; i N; i) { pi 4.0 / (1.0 x * x); }编译能通过运行也不报错但算出来的π可能每次都不同甚至最终值差得很离谱。这是因为多个线程同时读取并修改pi变量导致“读-改-写”三步并非原子操作你改完了别人也改完了后写的覆盖了先写的最终结果一团糟。解决数据竞争有几种常见手段我整理成一张速查表方案推荐度说明reduction(:var)高语法简洁性能好编译器自动处理归并omp critical中通用但性能开销大适合无法用reduction的场景omp atomic中高只针对单条语句开销比critical小锁omp_set_lock低功能强大但代码复杂容易死锁omp atomic和critical的区别在于atomic只保护单个内存操作适合极短语句#pragma omp atomic pi local_pi;但它要求右侧表达式不包含对pi的读取并且只支持加减乘除等简单运算局限性比较大。所以你能用reduction解决的就不要自己手动加锁。4. 进阶案例矩阵乘法与任务并行4.1 矩阵乘法的并行化思路矩阵乘法是数值计算里的“常客”机器学习、有限元、图形学全都能看到它的身影。假设有两个N×N的矩阵A和B结果矩阵C的定义是C[i][j] Σ A[i][k] * B[k][j] (k从0到N-1)这个计算天然适合并行因为C[i][j]的计算完全独立于其他元素。最简单的做法是把外层循环i交给多个线程每个线程负责计算一行结果#include stdio.h #include stdlib.h #include omp.h #define N 1000 int main() { double** A (double**)malloc(N * sizeof(double*)); double** B (double**)malloc(N * sizeof(double*)); double** C (double**)malloc(N * sizeof(double*)); for (int i 0; i N; i) { A[i] (double*)malloc(N * sizeof(double)); B[i] (double*)malloc(N * sizeof(double)); C[i] (double*)malloc(N * sizeof(double)); } // 初始化 for (int i 0; i N; i) { for (int j 0; j N; j) { A[i][j] rand() % 10; B[i][j] rand() % 10; C[i][j] 0.0; } } double start omp_get_wtime(); #pragma omp parallel for for (int i 0; i N; i) { for (int k 0; k N; k) { double temp A[i][k]; for (int j 0; j N; j) { C[i][j] temp * B[k][j]; } } } double end omp_get_wtime(); printf(矩阵乘法耗时: %f 秒\n, end - start); // 清理内存 for (int i 0; i N; i) { free(A[i]); free(B[i]); free(C[i]); } free(A); free(B); free(C); return 0; }我这里的循环顺序用了i-k-j而不是教科书里更直观的i-j-k这是一个性能优化的细节把最内层循环变成访问B矩阵的一整行连续内存可以大幅提升cache命中率。很多人写并行矩阵乘法发现效率提不起来问题往往不在并行部分而在内存访问模式上。编译运行gcc -fopenmp -O2 -o matmul matmul.c export OMP_NUM_THREADS4 time ./matmul在1000×1000矩阵规模下串行大约需要几秒4线程并行能把它压缩到1秒上下。这里的并行化“性价比”很高因为你几乎没改任何算法逻辑只是加了一行#pragma omp parallel for。4.2 深入parallel for的调度策略如果你仔细观察上面矩阵乘法的性能会发现不同线程计算完每一行的时间不完全相同。这很正常因为内存带宽、cache状态、系统中断都可能导致轻微的不均匀。OpenMP为此提供了schedule子句用来控制循环迭代如何在线程间分配。常见调度策略有static、dynamic和guided三种我直接说结论schedule(static, chunk_size)在循环开始前把迭代空间等分为若干块每块大小固定每个线程分到连续的几块。优点是开销极小适合每次迭代计算量接近的场景。schedule(dynamic, chunk_size)运行时动态领取任务线程做完一块就去领取下一块。适合迭代计算量波动大的场景但动态分配本身有额外开销。schedule(guided, chunk_size)动态调度的一种改进初始时分块很大越往后块越小用于平衡负载和调度开销。举个例子如果你想用dynamic调度#pragma omp parallel for schedule(dynamic, 16) for (int i 0; i N; i) { // ... }在矩阵乘法这种负载相对均衡的场景static往往表现更好而在处理稀疏图、不规则网格等问题时dynamic优势就出来了。建议你写一个小程序分别用不同schedule跑同一份数据对比耗时就一目了然。这里还有一个容易忽略的点如果你不指定scheduleGCC的默认策略通常是static具体取决于版本它已经能覆盖绝大多数场景。所以不要过度设计先跑出基线再尝试不同调度策略逐步优化。4.3 任务并行从数据并行到任务并行OpenMP不只是能把循环拆开它还有一套适合“任务树”结构的并行机制核心是#pragma omp task。我用一个递归计算斐波那契数列的例子来说明。斐波那契的递归定义是fib(n) fib(n-1) fib(n-2)两个子任务之间没有依赖关系非常适合用task#include stdio.h #include omp.h long fib(int n) { if (n 2) return n; long x, y; #pragma omp task shared(x) x fib(n - 1); #pragma omp task shared(y) y fib(n - 2); #pragma omp taskwait return x y; } int main() { double start omp_get_wtime(); long result; #pragma omp parallel { #pragma omp single result fib(30); } double end omp_get_wtime(); printf(fib(30) %ld, 耗时: %f 秒\n, result, end - start); return 0; }注意这里有几个关键点task指令把大括号后的代码块封装成一个任务可以被任意空闲线程执行shared(x)声明这个变量是多任务共享的否则默认按firstprivate处理会导致结果丢失taskwait会阻塞当前线程直到所有子任务完成。single指令保证初始任务只由一个线程创建避免递归被重复计算。编译运行对比串行版本你会发现随着n增大并行版的耗时相对串行的加速比越来越可观。这个例子告诉你OpenMP的应用范围绝不止是循环并行它还能处理递归分治、链表遍历、动态负载均衡等结构更复杂的计算模式。5. 常见问题与排查技巧5.1 编译阶段的高频报错与解决我在带团队的时候几乎每周都会碰到新人问OpenMP编译报错的问题。把最高频的几个问题整理成速查表报错信息原因解决办法undefined reference to omp_get_thread_num编译时忘了加-fopenmp重新编译并加上-fopenmpfatal error: omp.h: No such file or directory编译器不是GCC或者GCC版本过旧使用gcc或g编译升级GCCerror: ‘for’ loop initial declarations are only allowed in C99 or C11 mode在C99之前标准下于for内声明变量加-stdc99或-stdc11或者把声明提到循环外libgomp: Thread creation failed系统线程数限制或内存不足减少OMP_NUM_THREADS检查ulimit -u我特别想强调第一种undefined reference这类错误非常具有迷惑性因为它出现在链接阶段新手容易以为自己的代码调用了某个不存在的库。实际上唯一的病因就是你忘加-fopenmp。如果你用CMake或Makefile管理工程记得在编译选项和链接选项里同时加上这个参数。这里再提供一条判断GCC是否支持OpenMP的验证命令echo | gcc -fopenmp -x c - -E /dev/null 21 echo OpenMP supported || echo OpenMP not supported如果输出OpenMP supported说明你的编译器支持OpenMP接下来的一切问题都只是编译参数或代码写法问题。5.2 线程数设置不生效的几种情形你在运行程序时明明设置了export OMP_NUM_THREADS8但程序却只用了1个线程或者始终用满所有核心。这种问题我在实际项目中踩过好几次原因通常有以下几种。第一种代码里调用了omp_set_num_threads()它会覆盖环境变量的设置。如果你在代码里写死了线程数外部环境变量就无效了排查时要先检查代码里有没有这种“硬编码”。第二种程序卡在了串行区域。OpenMP的并行只作用于由omp parallel指令标记的代码块其余部分依然是单线程运行。如果程序的大部分时间都在串行区你自然看不到多线程效果。第三种某些IDE或远程调试环境没有正确传递环境变量。我在VSCode Remote-SSH里跑OpenMP程序时遇到过环境变量不生效的情况解决办法是在启动命令前显式export或者直接在代码里打印线程数来确认#pragma omp parallel { int tid omp_get_thread_num(); if (tid 0) { printf(实际线程数: %d\n, omp_get_num_threads()); } }另外要特别留意OMP_DYNAMIC变量。从OpenMP 3.0开始如果设置了OMP_DYNAMICTRUE运行时环境可能会根据当前系统负载动态调整线程数导致你设置的OMP_NUM_THREADS被“修正”。如果想强制稳定线程数可以设置export OMP_DYNAMICFALSE5.3 性能不升反降的诊断思路并行程序最诡异的问题不是报错而是“看起来并行成功了但性能反而更差了”。我从经验出发给出一个排查路径。第一步确认并行度真的上去了。用htop或perf top观察运行时的CPU占用率如果多个核心没有同时忙碌大概率是并行区域没有覆盖到热点代码或者线程数设得太少。第二步检查并行区域是否过小。OpenMP创建线程和合并结果是有固定开销的如果并行循环本身只需要1毫秒而线程创建销毁需要2毫秒那并行就是负优化。解决办法是放大并行粒度比如把多个小循环合并到一个并行区域内或者用#pragma omp parallel把线程先“预热”出来在内部用多个for。第三步观察是否有隐性串行化。比较常见的情况是临界区、原子操作和锁竞争比如我在前文提到的手动critical替代reduction如果频繁进入临界区性能就会下降好几个数量级。用perf工具能看到热点阻塞在哪里但更快的办法是直接检查代码里有没有过度的critical或锁操作。第四步考虑内存带宽和NUMA非统一内存访问的影响。在服务器上CPU访问本地内存和远端内存的速度差别很大。如果OpenMP的默认线程亲和性设置不合适线程可能频繁访问远端内存导致性能退化。可以通过设置OMP_PROC_BINDtrue和OMP_PLACEScores改善export OMP_PROC_BINDtrue export OMP_PLACEScores这组设置能让线程尽量绑定在物理核心上避免线程在核心间漂移带来的缓存失效和调度开销。5.4 内存与系统层面的其他坑最后分享几个不那么显眼但实际能绊倒人的问题。第一个是栈空间溢出。如果你的并行代码在函数里声明了很大的局部数组多线程时每个线程都会在栈上分配一份可能导致栈溢出而崩溃。解决办法是用ulimit -s加大栈限制或把大数组放到堆上malloc。第二个是环境变量OMP_STACKSIZE它控制每个线程自己的栈大小。默认值通常够用但如果你写递归深度很大的并行程序比如前面提到的fibonacci例子就需要主动调大线程栈export OMP_STACKSIZE16M第三个是超线程环境下的性能判断。超线程技术能让一个物理核心模拟出两个逻辑核心但这两个逻辑核心共享运算单元所以超线程对OpenMP这种计算密集型的程序提升有限甚至可能因为资源争抢导致性能下降。因此你会发现8核16线程的CPU跑OpenMP时最佳的线程数往往不是16而是8左右。实际项目中我建议用一个小程序测试不同线程数绘制“线程数-耗时”曲线找到拐点再决定生产环境的线程配置。结尾一些亲测有效的经验我在Ubuntu上折腾OpenMP也有好几年了最后分享几个纯经验主义的建议或许能帮你少走弯路。第一-fopenmp和-O2最好搭配使用否则某些编译器优化和并行化可能会互相踩脚反而发挥不出性能。第二写并行程序时先用串行版本跑一遍确认结果正确再逐步加入并行指令方便定位并行引入的bug。第三遇到性能问题时不要急着改算法先检查数据布局和内存访问模式很多时候“慢”并不是并行本身的问题而是cache命中率太低。如果你打算把OpenMP用在大项目里我强烈推荐配合CMake来管理编译流程只要在CMakeLists.txt里加上find_package(OpenMP)它就能自动处理编译器和链接器参数避免每次手动写命令行时遗漏东西。这个做法在团队协作时尤其省心因为每个人的编译环境可能略有差异CMake会统一帮你搞定。如果你的Ubuntu环境比较老或者在用某些精简版系统镜像编译OpenMP程序时遇到libgomp相关的错误先别急着下载额外的软件包跑一遍sudo apt install build-essential基本就能解决。如果确实需要更新GCC版本可以尝试sudo apt install gcc-12 g-12然后用gcc-12编译你的程序这样不会影响系统默认的编译器。OpenMP的入门门槛其实很低把#pragma omp parallel for用熟再理解reduction、schedule、critical这几个核心概念已经能解决大部分实际计算问题了。希望这篇分享能帮你少踩一些不必要的坑更顺畅地享受多核CPU带来的性能红利。
返回列表