Make Cheatsheet
Parallelism
Use this Make reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
-j, and choosing a number
-j N runs up to N recipes at once. -j with no number means unlimited, which
will fork-bomb a large tree.
make -j8 make -j$(nproc) # Linux make -j$(sysctl -n hw.ncpu) # macOS and BSD make -j$(getconf _NPROCESSORS_ONLN) # portable make -j$(nproc) -l$(nproc) # cap by load average too
| Flag | Effect |
|---|---|
-j N | up to N jobs |
-j | unlimited jobs, avoid |
-l N --load-average=N | do not start a new job while load ≥ N |
-O --output-sync=target | buffer each target's output, print it whole |
-O line -O recurse -O none | other output-sync granularities |
-k | keep going after a failure, build everything that still can |
-i | ignore all errors, rarely what you want |
Set a default in the makefile with MAKEFLAGS += -j8, but be aware that it
applies to every invocation including make clean, and that a -j from the
command line overrides it.
-j only helps when the dependency graph is wide. If make -j16 runs one job
at a time, the graph is a chain, and no flag fixes that. Look for a .PHONY
target that everything depends on, or a sub-make serialization.
Interleaved output, and -O
Parallel recipes write to the same terminal, so lines from different jobs interleave and a compiler error can end up split across two other jobs' output.
$ make -j4 b start c done b done a done
$ make -j4 -O c done b start b done a done
-O (short for -Otarget) buffers a target's entire output and flushes it as
one block when the target finishes, which is what you want in CI. Costs: nothing
appears until a target completes, so a long compile looks hung, and a recipe that
prompts for input will hang for real.
What breaks under -j
Serial makefiles hide bugs that -j exposes immediately. Every one of these is
a missing dependency edge.
| Symptom | Cause |
|---|---|
| "No such file or directory" for a file another rule creates | missing prerequisite, works serially by luck of ordering |
| Two jobs writing the same file | two rules produce one output, or a shared temp name |
mkdir: File exists | several rules mkdir the same directory |
| Random failures that vanish on rerun | an implicit ordering the graph does not express |
| A generated header compiled before it exists | the header is not a prerequisite of the objects |
Fixes, in order of preference:
# 1. Declare the real edge build/main.o: generated/version.h # 2. Order-only prerequisite for a directory build/%.o: src/%.c | build build: mkdir -p $@ # 3. mkdir -p is idempotent, so this is safe in a parallel recipe build/%.o: src/%.c @mkdir -p $(@D) # 4. Unique temp names, never a fixed one %.o: %.c $(CC) -c -o $@.tmp$$$$ $< && mv $@.tmp$$$$ $@ # 5. A rule that really does produce several files at once, GNU 4.3+ parser.c parser.h &: parser.y bison -d -o parser.c $<
Before &: existed, the portable form for a multi-output rule was a sentinel
file that both outputs depend on, which is why old makefiles have .stamp
targets.
.NOTPARALLEL and .WAIT
.NOTPARALLEL forces the entire makefile to run serially, whatever -j says.
.NOTPARALLEL:
$ make -f np.mk -j4 a done b start b done c done
GNU make 4.4 lets .NOTPARALLEL take prerequisites, so only those targets'
prerequisites are serialized. It is a blunt instrument, and reaching for it
usually means an unexpressed dependency. Fix the graph instead.
.WAIT, also GNU 4.4, is the surgical version. Put it in a prerequisite list and
everything before it finishes before anything after it starts.
all: schema .WAIT migrate .WAIT seed
$ make -f w.mk -j4 a done b done c done
.WAIT is a real target name, so older makes see it as a missing file and fail.
Guard it on $(MAKE_VERSION) if the makefile must run on make 4.3 or older.
Recursive make under -j
The job server is what keeps N sub-makes from each starting N jobs.
- Prefix any sub-make invocation with + when the line does not literally
contain $(MAKE), so make marks it as recursive and passes the job server
along in MAKEFLAGS.
- Never put -j inside a recipe. Pass it once on the outer command line.
- warning: jobserver unavailable: using -j1 on make 4.3 and older means a
sub-make missed the handoff. GNU make 4.4's FIFO-based job server removed most
of these.
- Sub-directory targets must be .PHONY, or a directory's timestamp will make
make skip the whole sub-build.
.PHONY: $(SUBDIRS) $(SUBDIRS): +$(MAKE) -C $@
Measuring whether it helped
time make -j1 && make clean time make -j8 make -j8 --debug=j 2>&1 | grep -c 'Putting child' # jobs actually launched make -n | wc -l # recipe lines in the plan make -d 2>&1 | grep 'Must remake target' # what is being rebuilt
Two structural checks worth running once on any slow build:
# a serial no-op build should be fast. If it is not, the graph is the problem time make # after a full build, expect under a second # find the long pole: --trace names each target right before its recipe make clean && make -j1 --trace
Practical ceilings: link steps are memory-bound and often the real limit, so
-j$(nproc) with 2 GB per link will swap. Compilation scales close to linearly
up to core count, and beyond core count -j only adds contention. On CI
containers nproc reports the host's cores rather than the cgroup quota, so
pin -j to the runner's advertised CPU count instead.