https://jmmv.dev/2022/06/autoconf-caching.html
# jmmv.dev
* About
* Blog
* Resume
* Software
* Archive
* Series
* Tags
[ ]
[search]
Speeding up autoconf with caching
June 17, 2022 * About 10 minutes * Tags: freebsd, netbsd
In the recent Remembering Buildtool post, I described how setting up
a cache of configuration checks was an important step in Buildtool's
installation process. The goal was to avoid pointless repetitive work
on every build by performing such common checks once.
Episode 457 of BSD Now featured my post and Allan Jude wondered how
much time would be saved in a bulk build of all FreeBSD packages if
we could just do that same kind of caching with GNU Autoconf. And,
you know what? It is indeed possible to do so. I had mentioned it en
passing in my post but I guess I wasn't clear enough, so let's
elaborate!
A blog on operating systems, programming languages, testing, build
systems, my own software projects and even personal productivity.
Specifics include FreeBSD, Linux, Rust, Bazel and EndBASIC.
[ ] Subscribe
0 subscribers
Follow @jmmv on Mastodon Follow @jmmv on Twitter RSS feed
The problem: Autoconf's slowness
The configure scripts generated by GNU Autoconf are slow, very slow,
to the point where sometimes their execution time is longer than the
time it takes to build the package they configure. This is especially
true on multi-core systems where these scripts make builds drag
along.
Here, take a look at some package build times on an 8-core machine
from 2011:
Package Type configure make -j8
bmake Small C package 8s 7s
coreutils Medium C package 62s 96s
m4 Small C package 36s 9s
pkgconf Small C package 3s 2s
kyua Small C++ package 6s 91s
tmux Small C package 7s 8s
For comparison, here are two of the builds above--I did not have the
patience to run them all--on an even older single-core PowerBook G4
from 2005:
Package Type configure make -j1
bmake Small C package 44s 60s
tmux Small C package 46s 217s
Note the huge cost of the configure run times relative to make.
You might think that slow configure scripts aren't a big deal, but
pause for a second to realize that these scripts plague the entire
Unix ecosystem. Almost every package in your standard Linux
distribution or BSD system has a configure script of its own, and
this script has to run before the package can be built. Considering
that this ecosystem favors small source packages, each with its own
configure script, the costs add up quickly.
But wait, it gets even worse. All BSD systems and some Linux
distributions have some form of bulk build: a long-running process
where they rebuild their entire collection of binary packages at once
from source. These binary packages are the ones you can later
trivially install via, say, pkg on FreeBSD or dnf on Fedora. These
bulk builds take several hours at best on the most powerful machines
and several weeks (or is it months?) at worst on legacy platforms. I
don't have hard numbers, but based on the simple data presented
above, I think it's fair to assume that a large percentage of the
total build time is wasted on configure scripts--and most of this time
is stupidly spent doing the same work over and over and over again.
Can we do anything to make these runs faster? Yes, it turns out we
can. But before getting into that, lets explore why these scripts are
so slow and why they are still a big problem even on modern
multi-core machines.
Why are configure scripts slow?
The reasons configure scripts are slow are varied:
1. They are huge shell scripts. bmake's configure to take just one
example is about 210kb with a total of 7500 lines. The shell is a
language that doesn't win any speed tests.
2. They are creating, compiling and running^1 small programs to
verify features of the host system.
3. They are sequential and mostly I/O-bound (again, they are shell
scripts), which is the worst kind of sequential.
'nuf said.
Parallel builds to the rescue?
"Ah, but it doesn't matter", you say. "While the configure script of
one package may be slow, we are building thousands of packages in a
bulk build. Therefore, we can make use of parallelism to hide the
costs!" Yeah… not really.
You see, the end-to-end build of a package tends to be bimodal: the
configure script is slow, I/O-bound, and sequential, while the build
itself is typically reasonably parallel and CPU-bound. (Actually, the
end-to-end process is trimodal if we account for the I/O-bound
installation step, but let's ignore that in this post.)
These different kinds of resource consumption at different stages
pose problems when trying to parallelize the build of independent
packages.
Suppose we have a machine with 8 CPUs and that every package's build
stage is parallel enough to consume up to 4 CPUs at any given time.
If we try to build 8 of these packages in parallel to paper over the
fact that configure is sequential, we'll have good cases where we are
running the 8 scripts at once and making an OK use of resources.
Unfortunately, we'll also have bad cases where the 8 packages are in
their build stage trying to use 4 CPUs each, which means we'll have
32 CPU-hungry processes scheduling on 8 CPUs. The latter scenario is
more likely than the former so this is not great.
To "fix" this under bulk build scenarios, we could say that we don't
want to allow parallel builds within a package (i.e. we restrict each
build to make -j1) to keep every package limited to one CPU at most.
But if we do that, we'll introduce major choke points in the bulk
build because some packages, like clang, are depended on by almost
everything and take forever to build without parallelism.
Repeated work
The worst part of all is that a lot of the work that configure
scripts do is pure waste. How many times do you really need to check
during the build of multiple packages that your system has a C
compiler? And stdlib.h? And uint8_t? And a Fortran compiler? FFS.
Most of these checks are useless in most packages (configure scripts
are cargoculted and almost nobody understands them), and for those
that are useful, their answers aren't going to change for the
duration of the build. Heck, the answers are likely not going to
change for the lifetime of the entire system either.
This is particularly frustrating when you want to revive an old
machine--like the PowerBook G4 I mentioned above--where the only option
to get modern software is to build it yourself. Doing so is
exasperating because you spend most of your time witnessing configure
scripts doing repeated work and very little time building the code
you want to run.
All hope is not lost though. I'm sure you have occasionally noticed
this: when you run configure in a large project that has recursive
configure invocations, you'll often see (cached) next to individual
checks. In other words, these scripts do know how to reuse results
from previous invocations.
So, wouldn't it be great if we convinced configure to avoid doing
repeated work across packages? Couldn't we check for these details
just once and reuse cached results later? Well, yes, yes we can!
Say hello to GNU Autoconf caching
GNU Autoconf does have first-class caching features. Using them
within a single package is trivial. All we have to do is pass the
--config-cache flag to the script as described in the Cache Files
section of the manual and it will maintain a config.cache file with
the results of the invocation. You can see the impact of a perfect
cache here:
.../m4-1.4.19$ time ./configure --config-cache >/dev/null
./configure > /dev/null 16.32s user 12.18s system 90% cpu 31.660 total
.../m4-1.4.19$ time ./configure --config-cache >/dev/null
./configure --config-cache > /dev/null 1.45s user 0.77s system 108% cpu 2.045 total
In other words, configure's time went from 31 seconds to just 2 by
saving and reusing the previous results. (Note that this is different
than running config.status, which just recreates output files, but
let's leave that aside.)
This is nice for a single package, but as it turns out, the
--config-cache flag takes an optional parameter to specify the path
to the cache file. There is nothing preventing us from passing a path
to a central location and reusing the same cache for various
packages!
In fact, the GNU Autoconf developers have thought about this problem.
On the one hand, the tool supports setting up a system-wide
configuration file (known as config.site) as described in the Setting
Site Defaults section of the manual. And on the other hand, the
default code snippet that they show in the manual has an explicit
mention of using a system-wide cache:
# Give Autoconf 2.x generated configure scripts a shared default
# cache file for feature test results, architecture-specific.
if test "$cache_file" = /dev/null; then
cache_file="$prefix/var/config.cache"
# A cache file is only valid for one C compiler.
CC=gcc
fi
It wouldn't be easier, really, to cache results, right? But then&
mldr; why aren't we collectively using this feature more widely?
Well, caching configure results willy-nilly can cause random build
failures because the checks performed by one package aren't
necessarily equivalent to similar-looking checks in another. An
obvious case is when the results of a check depend on the results of
a previous check: for cache correctness, any two scripts need to run
these two checks in the same order, but there is no guarantee that
they'll do so.
If we want to have system-wide caching of reasonable safety, we need
to do better than simply pointing all configure runs to a central
cache file. And this is where autoswc enters the picture.
Enter autoswc
autoswc, whose name stands for Automatic System-Wide Configuration
and was brought to you by yours truly in 2004, is a little tool that
exposes GNU Autoconf system-wide caching facilities in a safe manner.
The key idea behind autoswc is that you (the administrator) create a
system-wide configure script with the list of checks you want to
cache and then run autoswc to refresh the cache at specific points in
time (say before performing a bulk build). Then, any build you
perform from within pkgsrc (the tool is specific to this packaging
system), will reuse those checks--but these arbitrary builds won't
contaminate the central cache.
Put it another way: autoswc helps define a cache of safe checks and
automates the process of using those during bulk builds minimizing
the risks of bad things happening due to cache contamination.
Using this tool is easy. I had not used it in years, but installing
it from pkgsrc and setting it up only required these steps. Just like
with Buildtool, I'm surprised such old code of mine still works:
1. Install the pkgtools/autoswc package.
2. Optionally copy /usr/pkg/share/autoswc/configure.ac to /usr/pkg/
etc/autoswc/configure.ac and extend the sample script with the
checks you want to cache.
3. Append .sinclude "/usr/pkg/share/autoswc/autoswc.mk" to /etc/
mk.conf.
4. Occasionally run autoswc from the command line to update the
cache.
Voila. All package builds done through pkgsrc now benefit from the
cached configuration results generated by the files in step 2.
Unfortunately, as good as this may seem, autoswc's results aren't
impressive. The main problem is that it's on you (the administrator)
to curate the list of checks to cache. This is a very difficult task
as it requires looking at what configure scripts are doing throughout
a bulk build and determining which checks are safe to cache and which
aren't, and ain't nobody have time for that.
I think my hope when I created this tool was that we'd get a swarm of
people with pkgsrc expertise to curate the predefined list of checks
in the sample configure.ac file and then we'd all benefit from the
results on our own machines and on the bulk build clusters… but
this obviously did not happen. But the feature in GNU Autoconf
exists, autoswc is still functional and trivially configurable, and
with some effort it could potentially bring some tangible speed
improvements to builds--especially on old hardware.
Anyhow, now you know about one more "hidden" feature that GNU
Autoconf has and that can potentially speed things up in repeated
package builds massively.
Thanks for reading and enjoy the weekend!
---------------------------------------------------------------------
1. Avid readers will note that another consequence of running the
test programs that configure creates is that configure scripts
are often terrible when trying to cross-build software for other
platforms. The test programs must be built for the target system
in order to provide correct results, but that means that they
cannot be run on the host. -[?]
<< Previous
All posts
Next >>
(0)
(0)
Share on Reddit
Share on Hacker News
Share on Twitter
[pen]
[ ]
Your name (optional): [ ]
Your email (optional): [ ] Invisible to all
readers; if provided, you will receive notifications when replied to
(not implemented yet)
Post Comments are subject to moderation. This feature is experimental
and is powered by EndTRACKER. If you experience any issues, please
contact me off-band.
[20181124-s]
Julio Merino
A blog on operating systems, programming languages, testing, build
systems, my own software projects and even personal productivity.
Specifics include FreeBSD, Linux, Rust, Bazel and EndBASIC.
[ ] Subscribe
0 subscribers
Follow @jmmv on Mastodon Follow @jmmv on Twitter RSS feed
Featured software
* EndBASIC: Online BASIC+DOS env
* EndTRACKER: Services for static sites
Featured posts
* Fast machines, slow machines
* EndBASIC 0.10: Core language, evolved
* Farewell, Microsoft; hello, Snowflake!
* Rust is hard, yes, but does it matter?
* Rust traits and dependency injection
* A year on Windows: Introduction
* Always be quitting
* How does Google keep build times low?
* How does Google avoid clean builds?
* Unit-testing a console app (a text editor)
* More...
Back to top
Copyright 2004-2023 Julio Merino
[stamp]