news 2026/10/13 17:20:48

make -j 后面到底填几?核数、内存与负载的平衡之道

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
make -j 后面到底填几?核数、内存与负载的平衡之道

如果你和我一样,常年要在服务器和本机上手工编译各种源码,这个场景一定不陌生:configure 跑完,Makefile 生成完毕,你敲下 make,编译速度慢得像在数大米。于是果断 Ctrl+C,改成 make -j,然后开始纠结真正的灵魂问题——后面的数字到底填多少才合适?

这个问题看着简单,翻车率其实高得离谱。填少了,八核机器只跑俩进程,白白浪费算力;填大了,内存直接爆掉,swap 一顿狂转,编译速度反而比单线程还慢。更麻烦的是,同一台机器编译 C 项目和编译重模板 C++ 项目,最优的 -j 值可能是两套完全不同的答案。

这篇文章不打算讲 make 的底层源码,我以这些年实际编译各种 C/C++ 工程的经历,把 -j 数值背后的两件事彻底讲清楚:一是你的机器到底有多少个核能干活,二是你的内存、散热、负载允不允许把这些核全部喂饱。顺带会给出可以直接抄走的命令和判断方法,不管你是在 Linux 服务器上折腾,还是在 macOS 笔记本上编点东西,都能用上。

1. 先搞清楚 make -j 到底在并行什么

1.1 make 的工作方式:一条规则就是一个进程

很多人把 make -j 想成"make 会把一个编译任务拆成多线程来跑",这个理解其实不对。make 本身不编译任何东西,它只是一个调度器,读 Makefile 里的规则,看哪个目标需要重新生成,然后去执行规则里写的命令行。

拿最典型的 C/C++ 工程来说,Makefile 里通常有几百条这样的规则:每个 .o 文件对应一条编译命令,最后链接生成可执行文件又是一条命令。make 的职责就是判断依赖、决定执行顺序、调用 shell 去运行这些命令。你写make -j8,意思不是"把单个 gcc 命令拆成 8 个线程",而是"允许最多同时运行 8 条规则命令"。每一条规则命令,本质上都是一个独立的 gcc/g++ 子进程。

这一点决定了并行编译的收益来源:项目里那些互不依赖的 .o 文件,可以同时编译。一个 .cpp 文件从预处理到汇编,再到优化、生成机器码,整个过程绝大部分时间都消耗在单个 CPU 核上,gcc 内部虽然也有少量并行优化选项,但默认情况下一个编译单元就是一颗核在跑。串行编译时,你四核机器只有一颗核在干活,剩下三颗全程看戏,这就是为什么make和make -j4的耗时差距经常是三到四倍。

1.2 依赖图有多宽,并行就有多赚

既然 make -j 并行的是"互不依赖的规则",那并行度的上限就不是核数,而是依赖关系允许的并发任务数。一个项目的头文件包含关系和源文件依赖关系构成一张依赖图,某个瞬间能同时启动多少条编译命令,取决于这张图当前有多"宽"。

举个例子:1000 个相互独立的 .c 文件,每个文件编译只需 2 秒,串行要 2000 秒,32 核机器上-j32大概 63 秒就跑完,收益接近线性。但如果是那种 3 个巨大的翻译单元、每个要编译 10 分钟的工程,就算你有 64 核,-j64和-j3的差距也大不到哪去,因为同时能并行的工作最多就 3 条。这也是为什么有些冷门工程,你把 -j 从 4 调到 16,速度却纹丝不动——不是机器不行,是依赖图只有那么宽。

还有一个经常被忽略的环节:编译完成后的链接阶段。源文件可以并行编译,但最终链接可执行文件通常只有一个进程在做,哪怕你用了 lld 这类并行链接器,单条链接命令的并行度也有限。所以编译快结束时会看到 CPU 占用骤降,这是正常现象,不是机器出了问题。

2. 查核数的正确姿势:你看到的"核数"不一定对

2.1 各平台一条命令拿到核数

先解决最基础的问题——怎么知道机器有几个核。不同系统命令不一样,我把最常用的列出来:

系统命令返回内容
Linuxnproc当前进程可用的逻辑核数(最推荐)
Linuxlscpu插座数、每插座核数、每核线程数等详细信息
Linuxgetconf _NPROCESSORS_ONLN在线逻辑核数
Linuxgrep -c ^processor /proc/cpuinfo老办法,逻辑核数
macOSsysctl -n hw.ncpu逻辑核总数
macOSsysctl -n hw.physicalcpu物理核总数
Windows CMDecho %NUMBER_OF_PROCESSORS%逻辑核数

日常最省事的组合是nproc加上lscpu。nproc直接给你一个数字,可以直接写进make -j$(nproc)里用;lscpu则能看全貌。注意很多发行版默认没装lscpu,如果提示找不到命令,先装一下或者直接用nproc。

macOS 上hw.physicalcpu在部分老版本系统或某些虚拟化环境下可能取不到值,那时候就用hw.ncpu兜底,问题也不大。

2.2 逻辑核和物理核:超线程算不算数

现代 x86 处理器普遍支持超线程(SMT),一个物理核能同时跑两个逻辑线程。查出来的核数会有两套数字,比如某颗 8 核 16 线程的 CPU,nproc返回 16,物理核只有 8。

做数学运算、跑 benchmark 时,逻辑核和物理核的差别很微妙;编译场景下我的经验是:默认按逻辑核数来定 -j 上限,但内存紧张时按物理核数打折。原因是编译器进程是纯 CPU 密集计算,两个逻辑线程在同一物理核上确实能同时执行,吞吐量能吃到一些 SMT 红利,所以make -j16在一颗 8 核 16 线程的 CPU 上通常确实比-j8快,尤其是在源文件多、每个编译单元缓存命中率不高的场景。但如果每个编译器进程要吃掉 1GB 以上内存,逻辑核全开的内存压力会让物理核数成为更现实的参考线,这个放到下一节细说。

2.3 容器、云主机和虚拟机上的核数陷阱

这是我最想提醒的一个坑:容器里看到的核数,常常不等于你真正能用的核数。nproc在 Linux 上会考虑 CPU 亲和性,如果你用 taskset 把进程绑定到某几个核上,nproc会跟着变。但在容器场景下,如果限额是通过 cgroup 的 CPU 配额实现的(比如只允许使用 2 个核的计算时间),nproc很多时候依然会显示宿主机的全部核数,比如显示 32,实际你只能用到 2 核的算力。

遇到这种情况,手动查一下容器的 CPU 限额文件:cgroup v2 看/sys/fs/cgroup/cpu.max,cgroup v1 看/sys/fs/cgroup/cpu/cpu.cfs_quota_us和cpu.cfs_period_us。配额 100000、周期 100000 表示一个核;配额 200000 就是两个核。别傻乎乎按nproc给的 32 去设-j32,那只会让几百个编译器进程抢两个核的算力,上下文切换开销直接把你拖垮。

云主机和虚拟机同理。16 个 vCPU 不代表你独占 16 个物理核,隔壁租户的突发负载随时可能挤占你的算力。虚拟化环境里,-j值可以按 vCPU 数来设,但要有心理预期:实际跑不满。

3. 内存才是并行编译的隐形天花板

3.1 一个编译进程到底吃多少内存

很多人翻车都翻在一点上:光顾着数核,忘了算内存账。编译一个 .cpp 文件,编译器进程需要把源码、头文件、模板实例化结果、中间表示全都装进内存。不同语言、不同代码风格,单进程内存消耗天差地别:

  • 普通 C 代码:单进程通常 150MB 到 400MB,-O0更低,-O2配合 LTO 会更高。
  • 普通 C++ 代码:单进程 500MB 到 1GB 很常见。
  • 重模板 C++ 代码(大量 STL 容器、模板元编程、头文件泛滥的项目):单进程轻松突破 1.5GB,极端情况能到 3GB。

这就是为什么同样的机器,编译 C 项目-j16稳如老狗,换个 C++ 模板库立刻内存告急。有个简单的估算公式:安全并行数 ≈ 可用内存(GB)÷ 单编译进程峰值内存(GB),再和核数取最小值。比如 16GB 内存的机器,编译重模板 C++ 项目按单进程 1.5GB 算,安全并行数大概就是 10,保守一点取 8。如果机器上还开着浏览器、IDE,可用内存还要再扣掉几个 G。

3.2 当并行数超过内存容量,速度会不升反降

内存挤爆的连锁反应是很多新手没见过的:系统开始疯狂换页,swap 分区被塞满,所有编译进程都在等磁盘 I/O 而不是在算数,CPU 占用率反而掉下去。这时候你打开top会看到 load average 高得吓人,但 CPU 使用率只有百分之二三十——全在等 swap。更要命的是,Linux 内核的 OOM killer 会在内存耗尽的瞬间随机挑进程杀掉,运气不好,你编了十分钟的 gcc 进程直接被干掉,整个 build 失败。

我做过一次对比实验,同一台 8GB 内存的笔记本编译一个模板较多的 C++ 工程:-j8用了 27 分钟,-j16因为内存不够开始 swap,反而花了 46 分钟,-j4是 24 分钟。看到这个结果你就明白,并行数不是越大越好,超过某个点之后是在用时间换折腾。

3.3 亲手压测一轮,找到你机器的安全并行数

与其靠公式猜,不如直接压测一轮,也就十几分钟的事。先开一个终端跑free -h看内存,再开一个终端跑htop(没有就装一个)或top,按内存占用排序,然后从-j2开始逐级往上试:

for j in 2 4 8 16; do echo "===== jobs=$j =====" /usr/bin/time -f "elapsed: %e s, maxRSS: %M KB" make -j"$j" clean all >/dev/null done

观察两个指标:一是%e也就是实际耗时,二是%M也就是整个构建过程的内存峰值。当-j翻倍、耗时却不再明显下降,或者内存峰值快顶到物理内存上限时,上一个档位就是你这台机器在当前项目上的甜点值。这个通用方法比任何经验公式都准,因为它把内存、核数、缓存、散热全部综合测了一遍。

4. 核数不是万能的:哪些场景该打折,哪些场景能超卖

4.1 不同类型项目的最优 -j 参考表

直接抄作业环节。假设机器是正常的 8 核 16 线程、32GB 内存的 Linux 开发机,下面是几类典型项目我常用的初始值:

项目类型单进程内存经验值推荐 -j备注
小型纯 C 库(几百个文件以内)200-400MB$(nproc)内存基本不是瓶颈
中型 C++ 工具(STL 用得狠但模板不多)0.5-1GB$(nproc)或物理核数32GB 内存完全压得住
重模板 C++ / 头文件重度项目1-2GB+$(nproc)的一半左右内存优先,别贪核数
嵌入式交叉编译0.3-0.8GB$(nproc)注意输出目录磁盘 I/O
链接超大二进制单进程 2-4GB编译阶段正常,链接阶段会骤降给链接留点内存余量
大多是小文件的 IO 密集构建低可略微超过核数见 4.2

这个表的逻辑核心就一句话:先算内存账,再谈核数。C 项目内存轻,核数拉满没问题;C++ 模板项目内存重,核数必须打折。降到多少合适?我一般先用echo $(( $(nproc) / 2 ))起手,然后看内存余量决定要不要往上加。

4.2 什么时候可以超过核数跑

反直觉的是,某些场景下-j超过核数反而更好。关键在于任务的性质:如果任务大量时间在等待 I/O,而不是在占用 CPU,那么多开几个进程可以互相填补等待间隙。比如构建过程中有大量解压、拷贝、代码生成,或者源文件在网盘、网络文件系统上,编译进程经常卡在磁盘读写,这时候-j$(nproc)可能跑不满磁盘队列,适当增大到 1.5 倍核数反而能压榨出更多吞吐。

但要注意,现在很多机器用的是 NVMe 固态,单盘随机读性能极强,I/O 等待不再像机械硬盘时代那么突出。所谓"超卖"的收益越来越小,而且超卖会让 load average 飙升,影响同机其他任务。我的建议是:CPU 密集的编译不要轻易超卖,I/O 密集的预处理阶段可以超一点,但不要形成习惯。

4.3 笔记本和共享服务器要单独算账

笔记本是另一个被忽视的变量:散热和功耗墙。现代笔记本的 CPU 有功耗限制,全核满载时主频会掉,风扇咆哮,外壳烫得能煎蛋。这种情况下,-j16跑 16 个逻辑线程,每个线程分到的频率可能比-j8时低不少,最终总吞吐甚至不如少开几个核。我自己的经验是:笔记本编译优先用物理核数,别用逻辑核数;如果发现编译中途 CPU 温度顶到 90 度以上、主频掉到基频以下,果断减半。

共享服务器/CI 机器同理。机器不是只有你一个人在用的,你开-j64把整台机器占满,同事的构建、数据库、线上服务全被拖垮。这种环境下优先用-l负载参数限流(下一节细讲),同时可以用nice -n 10 make -j$(nproc) -l$(nproc)把进程优先级降下来,至少别跟别人的任务抢 CPU。

5. 让 make 自己看着办:-l 负载参数的组合用法

5.1 load average 是什么,make -l 怎么判断

-l是 GNU make 里一个很实用但经常被忽略的参数,它的作用是给 make 加一个"负载刹车"。使用方式是在-j后面再跟一个-l和一个数字,比如make -j16 -l16,含义是:最多允许 16 个并发任务,但如果当前系统的 load average 已经达到或超过 16,就暂时不要启动新任务,等负载降下来再说。

load average 是 Linux 系统的一个经典指标,在/proc/loadavg里能看到三个数字,分别代表 1 分钟、5 分钟、15 分钟的平均运行队列长度。可以粗略理解为"系统里有多少个任务在排队等 CPU"。单核机器 load average 超过 1 就说明 CPU 排队了;16 核机器 load average 16 说明所有核刚好占满。make 的-l参数正是拿这个全局指标做判断,动态决定要不要继续开新任务。

这和硬编码-j4有本质区别:-j4是死数字,不管机器现在忙不忙,永远是 4 个并发;-j16 -l16是"上限 16,但机器忙就自动少开",适合那种不能独占整机的环境。

5.2 -j 设上限、-l 做刹车:一条可复制的命令

实际使用中,我最推荐的组合是:

make -j$(nproc) -l$(nproc) --output-sync=recurse

拆开解释:-j$(nproc)设最大值,-l$(nproc)设负载阈值,--output-sync=recurse让并行编译时各任务的输出不要乱成一锅粥。在这条命令下,32 核机器可以放开跑到 32 路并行,但如果这时候系统已经有其他任务把 load 顶到 40,make 会自动减少并发,等其他任务跑完再继续加码。服务器上白天有人用、晚上没人用的场景特别好使,白天自动收敛,晚上自动跑满。

如果你担心模板项目内存爆掉,还可以把-j设成内存安全值,-l保持系统核数,比如:

make -j8 -l16 --output-sync=recurse

这样并发上限由内存决定,负载刹车由系统整体负载决定,两层保护各管一摊。注意不同 make 实现的支持程度不一样,make -v看到是 GNU Make 就没问题;macOS 自带的 make 是 GNU Make 的历史版本,一般也支持-l,但如果你用的是 BSD make,这个参数的行为可能会有差异。

5.3 -l 的局限:I/O 排队和负载噪声

-l不是银弹,它有两个明显局限。第一,load average 统计的"排队任务"包括正在等待磁盘 I/O 的进程,所以如果构建过程本身有大量磁盘读写,load 会被 I/O 等待抬高,make 会误以为 CPU 很忙而减少并发,导致 CPU 利用率上不去。偏偏我们的构建过程里 I/O 等待很常见,所以它会低估可用算力。

第二,-l是在启动新任务前检查一次负载,不会实时干预已经开始跑的任务。如果系统负载在编译过程中突然飙升(比如别人的任务启动了),正在运行的任务不会中断,只是后续新任务暂缓启动。这个延迟特性在很多场景下其实是优点,因为它避免了频繁启停造成的抖动。总的原则是:-l适合做防御性限流,不要把精确调优的希望全押在它身上。

6. 递归 make、jobserver 与几个容易翻车的细节

6.1 子目录递归 make:jobserver 让并行数不膨胀

大型项目普遍采用递归 make 结构:一个顶层 Makefile,然后进到各个子目录去调子 make。很多刚接触的人会担心:make -j16下,10 个子目录各调一次 make,是不是变成 10×16=160 个并发任务了?

答案是:只要子 make 是通过$(MAKE)调用的,就不会。GNU make 在检测到-j时会创建一个 jobserver 管道,所有子 make 共享同一个任务令牌池,总并发数始终被顶层的 -j 限制住,不会因为递归而膨胀。这是 GNU make 设计得最精巧的地方之一,也是为什么我在 6.2 会强调一件事:永远用$(MAKE)而不是裸写make。

如果某个子目录的规则里写了cd subdir && make -j8,或者用了make -C subdir而不是$(MAKE) -C subdir,jobserver 令牌就不会被继承,子 make 会认为自己是独立的顶层 make,重新按自己的规则开并发。顶层-j16加几个子目录的-j8,实际并发瞬间失控,轻则编译变慢,重则内存爆掉。这是一个特别隐蔽的坑,排查过很多次才定位到。

6.2 别用裸 make -j,也别忘 -O 给输出排队

裸make -j(不带数字)是一个经典陷阱。GNU make 在-j不带参数时会尽可能多地同时启动任务,相当于没有上限。在一个几千文件的工程上跑裸make -j,瞬间拉起几百个编译器进程,内存直接被打穿,整个系统都可能卡死。这是我见过初学者翻车最多的地方,务必记住:-j后面必须跟数字,要么用$(nproc)兜底。

另一个并行的副作用是日志乱序。串行编译时输出一行挨一行很整齐,一旦并行,几十个编译器的输出交错在一起,哪条命令对应哪个文件根本看不清,一旦报错很难定位。解决方案是加--output-sync,推荐--output-sync=recurse,它能让每个任务的输出作为一个整体连续打印,递归子目录也能保持这种分组。这个参数加了之后,并行编译的可读性会提升一个数量级。

6.3 一套直接照抄的通用编译配置

最后汇总一份我日常最常用的配置,按场景分成四档,可以直接复制改改就用:

# 场景一:本地开发机,纯 C 项目,内存充足 make -j$(nproc) --output-sync=recurse # 场景二:C++ 项目,内存 16GB 左右,别冒险 make -j$(($(nproc) / 2)) --output-sync=recurse # 场景三:共享服务器 / CI,担心影响别人 make -j$(nproc) -l$(nproc) --output-sync=recurse # 场景四:完全不清楚机器配置,第一次构建 nproc && free -h && make -j$(nproc) --output-sync=recurse

场景四的思路是先把机器底细摸清楚再动手。nproc看核数,free -h看内存,然后跑一次完整的-j$(nproc),编译过程中开另一个终端用htop盯着内存。如果内存占用一路飙到接近物理上限、swap 开始增长,立即 Ctrl+C,把-j减半重来。跑过两三轮之后,你就大概知道自己这台机器在当前项目上的最佳档位在哪了。

我自己现在的默认动作其实很简单:先nproc,再free -h,然后用make -j$(nproc) -l$(nproc) --output-sync=recurse开头。C 项目直接跑完,C++ 模板项目如果发现编译进程内存占用超过 1GB,就手动降到物理核数。这套流程看起来朴素,但我在不同机器、不同项目上反复验证过,它比任何花哨的调参技巧都稳。编译这事的底层逻辑从来不是"核数越多越好",而是"内存扛得住、散热跟得上、系统不被打扰"三者之间的平衡点。找到那个点,你的构建速度就已经超过大多数人了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 17:20:48

dumpsys netstats detail 全解:从UID流量记账到Android网络调试实战

如果你排查过“手机为什么跑了几百MB流量”这类问题,大概率已经和dumpsys netstats打过照面。这套命令背后,是 Android 从内核到 Framework 一条完整的流量统计链路,核心执行者就是 netd 守护进程。而我今天要重点拆解的,是其中信…

作者头像 李华
网站建设 2026/10/13 17:20:20

Linux时间日期指令详解:date、timedatectl与时间同步

1. 为什么Linux时间日期指令值得单独拿出来讲很多人在Linux上敲命令敲了几年,ls、cd、grep用得滚瓜烂熟,但一碰到时间日期相关的指令就开始凭记忆拼凑,date加什么参数、cal怎么显示某年某月、timedatectl和hwclock到底什么区别,经…

作者头像 李华
网站建设 2026/10/13 17:18:21

多Agent协作下的轻量Workflow设计:从if-else到显式流程编排

过去大半年,我一直在做一件不算性感但特别磨人的事:把一套靠人肉连线、层层 if/else 拼出来的自动化流水线,重构成真正由多个 Agent 协作驱动的系统。单跑任何一个 Agent 都很顺畅,但只要把三个以上的 Agent 串在一起,…

作者头像 李华
网站建设 2026/10/13 17:18:11

提示工程师饱和之后,AI落地才是真正的金矿

你可能也听说了,前两年被吹上天的“提示工程师”,现在成了招聘市场上最尴尬的角色之一。每一天都有无数人挤进生成式AI这个赛道,简历上写着“精通Prompt工程”“擅长调教大模型”,结果发现岗位早就从“高薪抢人”变成了“百里挑一…

作者头像 李华
网站建设 2026/10/13 17:17:03

消费级显卡搞定大模型本地训练:从硬件配置到微调落地全指南

讨论大模型本地训练,很多人第一反应是“门槛很高”。确实,如果要从头训练一个模型,或者跑百亿参数级别的全参数微调,单靠一台电脑基本没戏。但“本地训练”这件事被误解得有点过了——在当下的技术条件下,一个普通开发…

作者头像 李华