I'm sorry, but in the year 2025, this is ridiculous:
$ time ./configure
./configure 13.80s user 12.72s system 69% cpu 38.018 total
$ time make -j48
make all-recursive
make[1]: Entering directory '/home/tavianator/code/gnu/findutils'
Making all in gl
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/gl'
Making all in lib
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/gl/lib'
GEN alloca.h
GEN ctype.h
GEN endian.h
GEN dirent.h
GEN fcntl.h
GEN error.h
GEN float.h
GEN malloc/dynarray-skeleton.gl.h
GEN malloc/scratch_buffer.gl.h
GEN malloc/dynarray.gl.h
GEN inttypes.h
GEN langinfo.h
GEN limits.h
GEN locale.h
GEN math.h
GEN pthread.h
GEN sched.h
GEN selinux/selinux.h
GEN selinux/context.h
GEN selinux/label.h
GEN stddef.h
GEN stdio.h
GEN stdlib.h
GEN string.h
GEN strings.h
GEN sys/socket.h
GEN sys/stat.h
GEN sys/time.h
GEN sys/types.h
GEN sys/uio.h
GEN sys/utsname.h
GEN sys/wait.h
GEN time.h
GEN uchar.h
GEN unicase.h
GEN unictype.h
GEN uninorm.h
GEN unistd.h
GEN unistr.h
GEN unitypes.h
GEN uniwidth.h
GEN wchar.h
GEN wctype.h
make all-recursive
make[4]: Entering directory '/home/tavianator/code/gnu/findutils/gl/lib'
make[5]: Entering directory '/home/tavianator/code/gnu/findutils/gl/lib'
CC libgnulib_a-allocator.o
CC libgnulib_a-areadlinkat.o
CC libgnulib_a-areadlink.o
CC libgnulib_a-argmatch.o
CC libgnulib_a-argv-iter.o
CC libgnulib_a-basename-lgpl.o
CC libgnulib_a-openat-proc.o
CC libgnulib_a-bitrotate.o
CC libgnulib_a-btoc32.o
CC libgnulib_a-btowc.o
CC libgnulib_a-c-ctype.o
CC libgnulib_a-c-strcasecmp.o
CC libgnulib_a-c-strncasecmp.o
CC libgnulib_a-c-strcasestr.o
CC libgnulib_a-c-strstr.o
CC libgnulib_a-c32_apply_type_test.o
CC libgnulib_a-c32_get_type_test.o
CC libgnulib_a-c32isalnum.o
CC libgnulib_a-c32isalpha.o
CC libgnulib_a-c32isblank.o
CC libgnulib_a-c32iscntrl.o
CC libgnulib_a-c32isdigit.o
CC libgnulib_a-c32isgraph.o
CC libgnulib_a-c32islower.o
CC libgnulib_a-c32isprint.o
CC libgnulib_a-c32ispunct.o
CC libgnulib_a-c32isspace.o
CC libgnulib_a-c32isupper.o
CC libgnulib_a-c32isxdigit.o
CC libgnulib_a-c32tolower.o
CC libgnulib_a-c32width.o
CC libgnulib_a-canonicalize.o
CC libgnulib_a-careadlinkat.o
CC libgnulib_a-chdir-long.o
CC libgnulib_a-cloexec.o
CC libgnulib_a-close-stream.o
CC libgnulib_a-closein.o
CC libgnulib_a-closeout.o
CC libgnulib_a-cycle-check.o
CC libgnulib_a-opendir-safer.o
CC libgnulib_a-dirname.o
CC libgnulib_a-basename.o
CC libgnulib_a-dirname-lgpl.o
CC libgnulib_a-stripslash.o
CC libgnulib_a-endian.o
CC libgnulib_a-exitfail.o
CC libgnulib_a-fcntl.o
CC libgnulib_a-creat-safer.o
CC libgnulib_a-open-safer.o
CC libgnulib_a-fd-hook.o
CC libgnulib_a-fflush.o
CC libgnulib_a-file-set.o
CC libgnulib_a-filemode.o
CC libgnulib_a-filenamecat-lgpl.o
CC libgnulib_a-float.o
CC libgnulib_a-fopen-safer.o
CC libgnulib_a-fpurge.o
CC libgnulib_a-freadahead.o
CC libgnulib_a-freading.o
CC libgnulib_a-fseek.o
CC libgnulib_a-fseeko.o
CC libgnulib_a-fts.o
CC libgnulib_a-getprogname.o
CC libgnulib_a-gettime.o
CC malloc/libgnulib_a-dynarray_at_failure.o
CC malloc/libgnulib_a-dynarray_emplace_enlarge.o
CC malloc/libgnulib_a-dynarray_finalize.o
CC malloc/libgnulib_a-dynarray_resize.o
CC malloc/libgnulib_a-dynarray_resize_clear.o
CC malloc/libgnulib_a-scratch_buffer_grow.o
CC malloc/libgnulib_a-scratch_buffer_grow_preserve.o
CC malloc/libgnulib_a-scratch_buffer_set_array_size.o
CC libgnulib_a-hard-locale.o
CC libgnulib_a-hash.o
CC libgnulib_a-hash-pjw.o
CC libgnulib_a-hash-triple-simple.o
CC libgnulib_a-human.o
CC libgnulib_a-i-ring.o
CC libgnulib_a-ialloc.o
CC libgnulib_a-idcache.o
CC libgnulib_a-localcharset.o
CC glthread/libgnulib_a-lock.o
CC libgnulib_a-malloca.o
CC libgnulib_a-math.o
CC libgnulib_a-mbchar.o
CC libgnulib_a-mbrtoc32.o
CC libgnulib_a-mbrtowc.o
CC libgnulib_a-mbscasestr.o
CC libgnulib_a-mbslen.o
CC libgnulib_a-mbsrtoc32s.o
CC libgnulib_a-mbsrtowcs.o
CC libgnulib_a-mbsstr.o
CC libgnulib_a-mbswidth.o
CC libgnulib_a-mbszero.o
CC libgnulib_a-mbuiter.o
CC libgnulib_a-mbuiterf.o
CC libgnulib_a-modechange.o
CC libgnulib_a-mountlist.o
CC libgnulib_a-nstrftime.o
CC glthread/libgnulib_a-once.o
CC libgnulib_a-openat-die.o
CC libgnulib_a-openat-safer.o
CC libgnulib_a-opendirat.o
CC libgnulib_a-parse-datetime.o
CC libgnulib_a-progname.o
CC libgnulib_a-quotearg.o
CC libgnulib_a-realloc.o
CC libgnulib_a-reallocarray.o
CC libgnulib_a-safe-read.o
CC libgnulib_a-same-inode.o
CC libgnulib_a-save-cwd.o
CC libgnulib_a-savedir.o
CC libgnulib_a-selinux-at.o
CC libgnulib_a-se-context.o
CC libgnulib_a-se-label.o
CC libgnulib_a-se-selinux.o
CC libgnulib_a-setlocale_null.o
CC libgnulib_a-setlocale_null-unlocked.o
CC libgnulib_a-sockets.o
CC libgnulib_a-stat-time.o
CC libgnulib_a-stdlib.o
CC libgnulib_a-strnlen1.o
CC libgnulib_a-sys_socket.o
CC glthread/libgnulib_a-threadlib.o
CC libgnulib_a-time_rz.o
CC libgnulib_a-timespec.o
CC unicase/libgnulib_a-tolower.o
CC unictype/libgnulib_a-ctype_alnum.o
CC unictype/libgnulib_a-ctype_alpha.o
CC unictype/libgnulib_a-ctype_blank.o
CC unictype/libgnulib_a-ctype_cntrl.o
CC unictype/libgnulib_a-ctype_digit.o
CC unictype/libgnulib_a-ctype_graph.o
CC unictype/libgnulib_a-ctype_lower.o
CC unictype/libgnulib_a-ctype_print.o
CC unictype/libgnulib_a-ctype_punct.o
CC unictype/libgnulib_a-ctype_space.o
CC unictype/libgnulib_a-ctype_upper.o
CC unictype/libgnulib_a-ctype_xdigit.o
CC libgnulib_a-unistd.o
CC libgnulib_a-dup-safer.o
CC libgnulib_a-fd-safer.o
CC libgnulib_a-pipe-safer.o
CC unistr/libgnulib_a-u32-chr.o
CC unistr/libgnulib_a-u32-cpy.o
CC unistr/libgnulib_a-u32-pcpy.o
CC unistr/libgnulib_a-u32-strcat.o
CC unistr/libgnulib_a-u32-strlen.o
CC uniwidth/libgnulib_a-width.o
CC libgnulib_a-version-etc.o
CC libgnulib_a-version-etc-fsf.o
CC libgnulib_a-wctype-h.o
CC libgnulib_a-vsnzprintf.o
CC libgnulib_a-xmalloc.o
CC libgnulib_a-xalloc-die.o
CC libgnulib_a-xgetcwd.o
CC libgnulib_a-xsize.o
CC libgnulib_a-xstrtod.o
CC libgnulib_a-xstrtol.o
CC libgnulib_a-xstrtoul.o
CC libgnulib_a-xstrtol-error.o
CC libgnulib_a-xstrtoumax.o
CC libgnulib_a-yesno.o
CC fopen.o
CC asnprintf.o
CC mbsrtoc32s-state.o
CC mbsrtowcs-state.o
CC mktime.o
CC printf-args.o
CC printf-parse.o
CC strerror_r.o
CC vasnprintf.o
AR libgnulib.a
make[5]: Leaving directory '/home/tavianator/code/gnu/findutils/gl/lib'
make[4]: Leaving directory '/home/tavianator/code/gnu/findutils/gl/lib'
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/gl/lib'
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/gl'
make[3]: Nothing to be done for 'all-am'.
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/gl'
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/gl'
Making all in build-aux
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/build-aux'
make[2]: Nothing to be done for 'all'.
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/build-aux'
Making all in lib
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/lib'
CC buildcmd.o
CC dircallback.o
CC extendbuf.o
CC fdleak.o
CC listfile.o
CC findutils-version.o
CC printquoted.o
CC qmark.o
CC safe-atoi.o
CC splitstring.o
CC regextype.o
CC bugreports.o
AR libfind.a
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/lib'
Making all in find
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/find'
Making all in .
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/find'
CC ftsfind.o
CC finddata.o
CC fstype.o
CC parser.o
CC pred.o
CC exec.o
CC tree.o
CC util.o
CC sharefile.o
CC print.o
CC getlimits.o
AR libfindtools.a
CCLD find
CCLD getlimits
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/find'
Making all in testsuite
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/find/testsuite'
make[3]: Nothing to be done for 'all'.
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/find/testsuite'
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/find'
Making all in xargs
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/xargs'
Making all in .
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/xargs'
CC xargs.o
CCLD xargs
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/xargs'
Making all in testsuite
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/xargs/testsuite'
make[3]: Nothing to be done for 'all'.
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/xargs/testsuite'
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/xargs'
Making all in locate
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/locate'
echo '@set LOCATE_DB /usr/local/var/locatedb' > dblocation.texi.tmp
if test -f dblocation.texi && cmp dblocation.texi.tmp dblocation.texi >/dev/null ; then \
rm dblocation.texi.tmp ; \
else \
mv dblocation.texi.tmp dblocation.texi ; \
fi
make all-recursive
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/locate'
Making all in .
make[4]: Entering directory '/home/tavianator/code/gnu/findutils/locate'
rm -f updatedb
CC locate.o
CC frcode.o
find=`echo find|sed 's,x,x,'`; \
frcode=`echo frcode|sed 's,x,x,'`; \
copyright=`sed -n '/^# Copyright /{s/^..//;p;q;}' ./updatedb.sh \
grep .` || exit 1; \
sed \
-e "s,@""bindir""@,/usr/local/bin," \
-e "s,@""libexecdir""@,/usr/local/libexec," \
-e "s,@""LOCATE_DB""@,/usr/local/var/locatedb," \
-e "s,@""VERSION""@,4.10.0.51-1ef8-dirty," \
-e "s,@""PACKAGE_NAME""@,GNU findutils," \
-e "s,@""PACKAGE_BUGREPORT""@,[email protected]," \
-e "s,@""PACKAGE_BUGREPORT_URL""@,https://savannah.gnu.org/bugs/?group=findutils," \
-e "s,@""PACKAGE_URL""@,https://www.gnu.org/software/findutils/," \
-e "s,@""find""@,${find}," \
-e "s,@""frcode""@,${frcode}," \
-e "s,@""SORT""@,/usr/bin/sort," \
-e "s,@""SORT_SUPPORTS_Z""@,true," \
-e "s/@""COPYRIGHT""@/${copyright}/" \
./updatedb.sh > updatedb
CC word_io.o
chmod +x updatedb
CCLD frcode
CCLD locate
make[4]: Leaving directory '/home/tavianator/code/gnu/findutils/locate'
Making all in testsuite
make[4]: Entering directory '/home/tavianator/code/gnu/findutils/locate/testsuite'
make[4]: Nothing to be done for 'all'.
make[4]: Leaving directory '/home/tavianator/code/gnu/findutils/locate/testsuite'
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/locate'
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/locate'
Making all in doc
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/doc'
ln -s ../locate/dblocation.texi dblocation.texi
make all-am
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/doc'
make[3]: Nothing to be done for 'all-am'.
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/doc'
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/doc'
Making all in po
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/po'
make[2]: Nothing to be done for 'all'.
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/po'
Making all in m4
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/m4'
make[2]: Nothing to be done for 'all'.
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/m4'
Making all in gnulib-tests
make[2]: Entering directory '/home/tavianator/code/gnu/findutils/gnulib-tests'
GEN arpa/inet.h
GEN sys/ioctl.h
GEN signal.h
## ---------------------------------------------------- ##
GEN sys/select.h
GEN sys/random.h
## ------------------- Gnulib tests ------------------- ##
## You can ignore compiler warnings in this directory. ##
## ---------------------------------------------------- ##
make all-recursive
make[3]: Entering directory '/home/tavianator/code/gnu/findutils/gnulib-tests'
Making all in .
make[4]: Entering directory '/home/tavianator/code/gnu/findutils/gnulib-tests'
CC locale.o
CC arpa_inet.o
CC c32tob.o
CC binary-io.o
CC concat-filename.o
CC dtotimespec.o
CC fd-safer-flag.o
CC imaxtostr.o
CC dup-safer-flag.o
CC inttostr.o
CC offtostr.o
CC umaxtostr.o
CC uinttostr.o
CC ioctl.o
CC localename.o
CC localename-unsafe.o
CC localename-table.o
CC nanosleep.o
CC priv-set.o
CC pthread-rwlock.o
CC tempname.o
CC glthread/thread.o
CC time.o
CC timespec-add.o
CC timespec-sub.o
CC tmpdir.o
CC unistr/u32-set.o
CC unlinkdir.o
CC xconcat-filename.o
CC test-localcharset.o
AR libtests.a
CCLD current-locale
CCLD test-localcharset
make[4]: Leaving directory '/home/tavianator/code/gnu/findutils/gnulib-tests'
make[3]: Leaving directory '/home/tavianator/code/gnu/findutils/gnulib-tests'
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils/gnulib-tests'
make[2]: Entering directory '/home/tavianator/code/gnu/findutils'
make[2]: Leaving directory '/home/tavianator/code/gnu/findutils'
make[1]: Leaving directory '/home/tavianator/code/gnu/findutils'
make -j48 12.05s user 4.70s system 593% cpu 2.822 total
I paid good money for my 24 CPU cores, but ./configure
can only manage to use 69% of one of them.
As a result, this random project takes about 13.5× longer to configure the build than it does to actually do the build.
The purpose of a ./configure
script is basically to run the compiler a bunch of times and check which runs succeeded.
In this way it can test whether particular headers, functions, struct fields, etc. exist, which lets people write portable software.
This is an embarrassingly parallel problem, but Autoconf can't parallelize it, and neither can CMake, neither can Meson, etc., etc.
The problem is that most build configurations scripts pretty much look like this:
CFLAGS="-g"
if $CC $CFLAGS -Wall empty.c; then
CFLAGS="$CFLAGS -Wall"
fi
...
: >config.h
if $CC $CFLAGS have_statx.c; then
echo "#define HAVE_STATX 1" >>config.h
else
echo "#define HAVE_STATX 0" >>config.h
fi
...
This is written in an inherently sequential way, but in principle many of these tests could be run in parallel.
In fact, we already have an effective tool for parallelizing lots of commands (make
), so let's use it.
We'll have a configuration makefile that generates our Makefile
and config.h
:
configure.mk
# The default goal generates both outputs, and merges the logs together
config: Makefile config.h
cat Makefile.log config.h.log >[email protected]
rm Makefile.log config.h.log
To start with, we'll save the initial values of variables like CC
and CFLAGS
into the Makefile
:
configure.mk
# Default values, if unspecified
CC ?= cc
CPPFLAGS ?= -D_GNU_SOURCE
CFLAGS ?= -g
LDFLAGS ?=
# Export these through the environment to avoid stripping backslashes
export _CC=${CC}
export _CPPFLAGS=${CPPFLAGS}
export _CFLAGS=${CFLAGS}
export _LDFLAGS=${LDFLAGS}
Makefile:
printf 'CC := %s\n' "$$_CC" >$@
printf 'CPPFLAGS := %s\n' "$$_CPPFLAGS" >>$@
printf 'CFLAGS := %s\n' "$$_CFLAGS" >>$@
printf 'LDFLAGS := %s\n' "$$_LDFLAGS" >>$@
Using export
like this avoids stripping the necessary backslashes from invocations like
$ ./configure CPPFLAGS='-DMACRO=\"string\"'
Now let's check which flags our compiler supports. We'll use this helper script:
flags.sh
#!/bin/sh
set -eu
VAR="$1"
FLAGS="$2"
shift 2
if "$@" $FLAGS; then
printf '%s += %s\n' "$VAR" "$FLAGS"
fi
When we run
$ ./flags.sh CFLAGS -Wall cc empty.c
it will print
CFLAGS += -Wall
if cc empty.c -Wall
succeeds (and nothing otherwise).
We can use this to generate some makefile fragments that enable only the supported flags.
configure.mk
ALL_FLAGS = ${CPPFLAGS} ${CFLAGS} ${LDFLAGS}
# Run the compiler with the given flags, sending
#
# - stdout to foo.mk (e.g. CFLAGS += -flag)
# - stderr to foo.mk.log (e.g. error: unrecognized command-line option ‘-flag’)
# - the compiled binary to foo.mk.out
# - but then we delete it immediately
TRY_CC = ${CC} ${ALL_FLAGS} empty.c -o [email protected] >$@ 2>[email protected] && rm -f [email protected] [email protected]
deps.mk:
./flags.sh CPPFLAGS "-MP -MD" ${TRY_CC}
Wall.mk:
./flags.sh CFLAGS -Wall ${TRY_CC}
pthread.mk:
./flags.sh CFLAGS -pthread ${TRY_CC}
bind-now.mk:
./flags.sh LDFLAGS -Wl,-z,now ${TRY_CC}
Each of these targets generates a tiny makefile fragment that's responsible for a single flag.
Importantly, each one can run independently, in parallel.
Once they're done, we can merge them all into the main Makefile
and clean up the cruft:
configure.mk
FLAGS := \
deps.mk \
Wall.mk \
pthread.mk \
bind-now.mk
Makefile: ${FLAGS}
printf 'CC := %s\n' "$$_CC" >$@
...
cat ${FLAGS} >>$@
cat ${FLAGS:%=%.log} >[email protected]
rm ${FLAGS} ${FLAGS:%=%.log}
The last part to add to the Makefile
is the part that actually builds our application.
We can write a simple makefile like this:
main.mk
OBJS := main.o
app: ${OBJS}
${CC} ${CFLAGS} ${LDFLAGS} ${OBJS} -o $@
${OBJS}:
${CC} ${CPPFLAGS} ${CFLAGS} -c ${@:.o=.c} -o $@
-include ${OBJS:.o=.d}
And append it to the Makefile
after all the flags:
We also want to generate a config.h
file, which defines macros that tell us whether certain libraries/headers/functions/struct fields/etc. exist.
We can do this by test-compiling some simple C programs.
As an example, these programs check for the various ways to learn about a file's creation timestamp:
define.sh
#!/bin/sh
set -eu
MACRO=$1
shift
if "$@"; then
printf '#define %s 1\n' "$MACRO"
else
printf '#define %s 0\n' "$MACRO"
fi
will output things like
#define HAVE_STATX 1
or
#define HAVE_ST_BIRTHTIM 0
depending on whether the build succeeds. We can use it in a makefile like this:
configure.mk
# Use a recursive make to pick up our auto-detected *FLAGS from above
config.h: Makefile
+${MAKE} -f header.mk $@
header.mk
# Get the final *FLAGS values from the Makefile
include Makefile
# We first generate a lot of small headers, before merging them into one big one
HEADERS := \
have_statx.h \
have_st_birthtim.h \
have_st_birthtimespec.h \
have___st_birthtim.h
# Strip .h and capitalize the macro name
MACRO = $$(printf '%s' ${@:.h=} | tr 'a-z' 'A-Z')
ALL_FLAGS = ${CPPFLAGS} ${CFLAGS} ${LDFLAGS}
${HEADERS}:
./define.sh ${MACRO} ${CC} ${ALL_FLAGS} ${@:.h=.c} -o [email protected] >$@ 2>[email protected]
rm -f [email protected] [email protected]
And to join them all together (along with a header guard):
header.mk
config.h: ${HEADERS}
printf '#ifndef CONFIG_H\n' >$@
printf '#define CONFIG_H\n' >>$@
cat ${HEADERS} >>$@
printf '#endif\n' >>$@
cat ${HEADERS:%=%.log} >[email protected]
rm ${HEADERS} ${HEADERS:%=%.log}
The last step is to wrap configure.mk
in a shell script, so people can run ./configure
like they're used to:
configure
#!/bin/sh
set -eu
# Guess a good number for make -j<N>
jobs() {
{
nproc \
|| sysctl -n hw.ncpu \
|| getconf _NPROCESSORS_ONLN \
|| echo 1
} 2>/dev/null
}
# Default to MAKE=make
MAKE="${MAKE-make}"
# Set MAKEFLAGS to -j$(jobs) if it's unset
export MAKEFLAGS="${MAKEFLAGS--j$(jobs)}"
$MAKE -r -f configure.mk "$@"
I put together a simple proof-of-concept GitHub repository that contains the full version of all these files if you want to copy-paste. The demo app prints file creation times, if it can figure out how to on your platform.
I've also been using a similar build system in bfs for a while, if you want to see a larger example. The performance benefit is substantial:
$ time ./configure
./configure 1.44s user 1.78s system 802% cpu 0.401 total
tavianator@tachyon $ time make -j48
make -j48 1.89s user 0.64s system 817% cpu 0.310 total
Of course, a lot of the benefit comes from just doing less configuration steps, but the 802% CPU use is a tremendous improvement over everything else I've tried.