Despite its old age, make is one of the main build automation tools in use. It has an outstanding reputation as a simple yet powerful tool. Software developers and DevOps engineers alike hold it in high regard. Makefiles offer an easy way to orchestrate a build pipeline and the prerequisite steps.

This post will introduce you to the art of writing a makefile. We will discover how to use makes parallelization features. That will help us harness the power of multi processor systems. Before I go ahead, I want to outline a demo scenario.
If you are in a hurry, feel free to jump to the code part and skip the intro.

Scenario

For demo’s sake, we will keep it simple. Imagine a project targeting multiple platforms. Let’s say macOS, linux, and windows for this example. The project uses make to execute build commands for each platform.

# makefile
# sleep represents work
build_job_mac:
	@echo "[MAC] building..."
	@sleep 10
	@echo "[MAC] DONE!"

build_job_linux:
	@echo "[LINUX] building..."
	@sleep 10
	@echo "[LINUX] DONE!"

build_job_win:
	@echo "[WIN] building..."
	@sleep 10
	@echo "[WIN] DONE!"

build: build_job_mac build_job_linux build_job_win

Each build will take ten seconds. That leaves us with 30 seconds to finish the execution of all build targets. That is not necessary since a multi processor system executes the work.

To speed-up the pipeline, we want to execute the build tasks in parallel.

Makefile

Make offers a solid mechanism to control parallel task execution. A look into the documentation helps to identify the needed options.

$ make --help
Usage: make [options] [target] ...
Options:
...
  -j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.
  -k, --keep-going            Keep going when some targets can't be made.
  -l [N], --load-average[=N], --max-load[=N]
                              Don't start multiple jobs unless load is below N.
...

Parallel execution

The –jobs parameter regulates the number of jobs executed parallel. One job per processor is a good ratio here. To acquire the number of available processors, use one of the following commands:

# linux
grep -c processor /proc/cpuinfo

# macOS
sysctl -n hw.physicalcpu

Now we can extend the initial makefile to execute the pipeline steps in parallel. The following snippet shows the additions for a system running on macOS.

# makefile
...

build_parallel: build_job_mac build_job_linux build_job_win

build:
	@$(MAKE) build_parallel -j$(shell sysctl -n hw.physicalcpu)
	@echo "[ok]"

Limit execution

Heavy system load is a common problem with parallel execution. The more operations a system has to handle, the more the load increases. With increasing load, everything slows down.

Make’s –max-load option is a great way to prevent such a thing from happening. That limits the number of jobs make runs at once, based on system load.

# makefile
...
build:
	@$(MAKE) build_parallel -j$(shell sysctl -n hw.physicalcpu) -l 1.75
	@echo "[ok]"

With this limitation in place, it is time to execute the pipeline. We notice that the execution phase takes only a fraction of the initial time consumed. In the outlined scenario, the savings equal to 2/3 of the initial time. That’s an immense saving in time and thus connected resources as well. What a great success!

Taken together, these results show how powerful make and its parallelization features are.

Here is the complete makefile:

build_job_mac:
	@echo "[MAC] building..."
	@sleep 10
	@echo "[MAC] DONE!"

build_job_linux:
	@echo "[LINUX] building..."
	@sleep 10
	@echo "[LINUX] DONE!"

build_job_win:
	@echo "[WIN] building..."
	@sleep 10
	@echo "[WIN] DONE!"

build_parallel: build_job_mac build_job_linux build_job_win

build:
	@$(MAKE) build_parallel -j$(shell sysctl -n hw.physicalcpu) -l 1.75
	@echo "[ok]"

Congratulations, you did it. You hopefully got a better understanding of make and how to use its parallelization features.

Going one step further

You could improve the above example by:

  • using the –keep-going modifier
  • add a prerequisites target that executes before the build target
  • use order-only targets and normal targets as prerequisites

Let me end this excursion with a quote for your meditation

We stand at the threshold of a many core world. The hardware community is ready to cross this threshold. The parallel software community is not.
— Tim Mattson