Re: [PATCH v2 2/6] Makefile: Run the subdirectory recursion in parallel

Sam James <[email protected]>
Newsgroups gmane.comp.lib.glibc.alpha
Organization Gentoo
Message-ID <[email protected]>
Adhemerval Zanella <[email protected]> writes:

> The top-level makefile was marked .NOTPARALLEL and ran the
> per-subdirectory sub-makes strictly one at a time in the topological
> order computed by scripts/gen-sorted.awk.  Only the compilations inside
> a single subdirectory could run in parallel, so on wide machines every
> subdirectory's compile tail and link steps left most cores idle, once
> per subdirectory per pass.
>
> Drop .NOTPARALLEL and encode the ordering the serial recursion relied
> on as explicit dependencies between the per-subdirectory targets:
>
>   * The subdirectories that generate shared files in $(common-objpfx)
>     consumed by the rest of the build without explicit dependencies run
>     serially, in their sorted order, before the rest fan out: csu
>     provides the tree-wide gen-as-const headers, and on Hurd the mach
>     and hurd directories generate the MiG RPC headers (every other
>     subdirectory otherwise runs a nested make in hurd/ to create them,
>     racing under parallel recursion; see sysdeps/mach/hurd/Makefile).
>     The first of them also materializes the other shared generated files
>     (abi-versions.h, sysd-syscalls, before-compile headers).
>
>   * The edges requested by the Depend files (now emitted by
>     gen-sorted.awk as subdir-deps-*) are preserved.  Edges pointing to
>     elf are dropped, as the sorted list already overrides them by
>     forcing elf last.
>
>   * The tests and xtests classes only run the per-directory test
>     programs, which are mutually independent once the others pass has
>     built the tree.  They therefore carry only the others pass barrier
>     below and none of the csu-first or Depend edges (+ordered_parallel_-
>     subdir_targets excludes them); otherwise "make subdir/tests" would
>     also run the tests of every subdirectory reachable through those
>     edges, rather than just the requested one.
>
>   * elf stays last: its rtld link consumes $(common-objpfx)libc_pic.a,
>     which aggregates every other subdirectory's objects, and its
>     rtld-Rules recursion compiles into the other subdirectories' object
>     directories.
>
>   * Pass barriers replace the implicit pass ordering: others after lib
>     (a subdirectory others sub-make would otherwise race to link
>     libc.so itself), tests/xtests after others, and the testroot
>     install behind others.
>
>   * The threading (nptl, or htl on Hurd) and realtime (rt) tests are
>     timing-sensitive and were previously shielded from system load by
>     the global .NOTPARALLEL.  With the recursion now parallel, a full
>     test run ('make check'/'tests', run-built-tests=yes) orders them
>     after the rest of the test run and one group at a time -- the
>     threading subdirectory, then rt -- and each serializes its own run
>     via a .NOTPARALLEL in its Makefile.  A targeted 'make subdir/tests'
>     is not ordered.
>
>     The serialization (the per-subdirectory .NOTPARALLEL and the ordering
>     above) constrains only the test run, not the build of the test
>     programs; but building and running a subdirectory's tests are fused
>     in its sub-make, so under run-built-tests=yes the serialized
>     subdirectories would also build their test programs serially.  To
>     avoid that, the top-level 'make check' (in Makerules) now runs two
>     passes: it builds every test program with run-built-tests=no, where
>     the recursion is fully parallel and none of the serialization
>     applies, and then runs the tests with run-built-tests=yes.  'make
>     tests' and a subdirectory's own 'check' stay single pass.
>
>   * The subdirectory-built files that the top-level libc.so and
>     linkobj/libc_pic.a rules list as prerequisites (elf/ld.so,
>     interp.os, sofini.os, sunrpc/librpc_compat_pic.a, and on Hurd
>     mach/libmachuser_pic.a and hurd/libhurduser_pic.a, from which the
>     lib*user-link.so inputs of libc.so are built) get order-only edges
>     on the corresponding sub-make with an explicit empty recipe.  A
>     prerequisite-only rule would trigger an implicit rule search and
>     this level would compile them itself in the wrong context.
>
>   * The install, clean, abi, and stubs target classes keep the
>     previous total order via a serial dependency chain.
>
>   * The elf DSO sorting test recipes, run when make remakes the
>     included generated makefiles at parse time, create the elf object
>     directory before writing into it; the serial recursion no longer
>     guarantees another rule created it first.
>
>   * catgets builds locale-specific message catalogs (and tst-catgets
>     reads one) by running gencat under de_DE.ISO-8859-1, hr_HR.ISO-8859-2
>     and ja_JP.SJIS, but never declared those locales as prerequisites: it
>     relied on localedata running before it in the serial order.  Under
>     the parallel recursion gencat could run before localedata generated
>     the locale, fall back to C, and fail.  catgets/Makefile now pulls the
>     locales in via gen-locales.mk, like the other subdirectories that use
>     locales in their tests.
>
> Results on a x86_64 machine [1] with default configuration [3]: a
> from-scratch build improves from 78.728s to 61s, and check with
> run-built-tests=no from 374s to 190s.
>
> On a 80-core aarch64 machine [2] with default configuration [3]: a
> from-scratch build improves from 105.251s to 56.703s, and check with
> run-built-tests=no from 886.183s to 298.726s.
>
> Build results are unchanged: all 8919 built objects, archives, and
> shared objects are bit-identical to the serial build across 7 clean
> parallel builds, the installed tree layout is identical, and the
> tests.sum failure sets are identical.  i686-gnu was verified with
> repeated from-scratch builds.
>
> [1] Ryzen 5900x, 12c/24t, gcc 16.1.1, binutils 2.26, and GNU make 4.3
> [2] N1, 80c, gcc 15.1.1, binutils 2.25, GNU make 4.3
> [3] --enable-stack-protector=all --enable-bind-now=yes --enable-profile=yes
>     --enable-fortify-source=2 --enable-hardcoded-path-in-tests
> ---
>  Makefile         | 138 +++++++++++++++++++++++++++++++++++++++++++++--
>  Makerules        |  31 +++++++++--
>  catgets/Makefile |  14 +++--
>  elf/Makefile     |   5 ++
>  htl/Makefile     |   7 +++
>  rt/Makefile      |   8 +++
>  6 files changed, 193 insertions(+), 10 deletions(-)
>
> diff --git a/Makefile b/Makefile
> index 6b0e0555189..45dc36942a8 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -54,9 +54,6 @@ configure: configure.ac aclocal.m4; $(autoconf-it)
>  endif # $(AUTOCONF) = no
>  
>  
> -# We don't want to run anything here in parallel.
> -.NOTPARALLEL:
> -
>  # These are the targets that are made by making them in each subdirectory.
>  +subdir_targets	:= subdir_lib objects objs others subdir_mostlyclean	\
>  		   subdir_clean subdir_distclean subdir_realclean	\
> @@ -129,6 +126,13 @@ lib-noranlib: subdir_lib
>  ifeq (yes,$(build-shared))
>  # Build the shared object from the PIC object library.
>  lib: $(common-objpfx)libc.so $(common-objpfx)linkobj/libc.so
> +ifdef libc.so-version
> +# Every program linked in the others pass lists the versioned name
> +# (through link-libc-between-gnulib) as a prerequisite, and the rule
> +# creating the symbolic link is visible in every sub-make.  Build it
> +# here once so the concurrent sub-makes do not race to create it.
> +lib: $(common-objpfx)libc.so$(libc.so-version)
> +endif
>  endif # $(build-shared)
>  
>  # Used to build testrun.sh.
> @@ -490,6 +494,134 @@ subdir=$(@D)$(if $($(@D)-srcdir),\
>  endef
>  
>  .PHONY: $(+subdir_targets) $(all-subdirs-targets)
> +
> +# Encode the topological ordering computed by scripts/gen-sorted.awk as
> +# explicit dependencies between the per-subdirectory targets, so that
> +# independent subdirectories build concurrently:

I suggest: ". In summary:"

It makes clear that there's independent comments below about the same thing.

> +#
> +#  * Every subdirectory depends on the first sorted one (csu, or mach on
> +#    Hurd): its sub-make also materializes the shared generated files in
> +#    $(common-objpfx) (abi-versions.h, sysd-syscalls, before-compile
> +#    headers, ...) that concurrent sub-makes would otherwise race to
> +#    create.
> +#
> +#  * The edges requested by the Depend files (emitted by gen-sorted.awk
> +#    as subdir-deps-*) are preserved.
> +#
> +#  * elf stays last, as in the sorted list.  Its rtld build recurses into
> +#    the other subdirectories' object directories via elf/rtld-Rules.
> +#
> +#  * Only target classes without cross-directory file conflicts use this
> +#    sparse ordering; everything else (install, clean, abi, stubs) keeps
> +#    the previous total order via a serial chain.
> +
> ++parallel_subdir_targets := \
> +  subdir_lib \
> +  objects \
> +  objs \
> +  others \
> +  tests \
> +  xtests \
> +  subdir_objs \
> +  # +parallel_subdir_targets
> ++serial_subdir_targets := $(filter-out $(+parallel_subdir_targets),\
> +				       $(+subdir_targets))
> +
> +# The tests and xtests classes run, rather than build, the per-directory
> +# test programs; once the 'others' pass barrier below has built the tree
> +# they are mutually independent and carry no cross-directory ordering.
> +# Keeping them out of the generated-file and Depend edges below is what
> +# lets 'make subdir/tests' run only that subdirectory's tests.
> ++barrier_only_subdir_targets := tests xtests
> ++ordered_parallel_subdir_targets := \
> +  $(filter-out $(+barrier_only_subdir_targets),$(+parallel_subdir_targets))
> +
> +# The subdirectories that generate shared files in $(common-objpfx)
> +# consumed by the rest of the build without explicit dependencies: csu
> +# provides the gen-as-const headers, and on Hurd the mach and hurd
> +# directories generate the MiG RPC headers (every other subdirectory
> +# otherwise runs a nested make in hurd/ to create them, racing under
> +# parallel recursion; see sysdeps/mach/hurd/Makefile).  Run them serially,
> +# in their sorted order (mach, hurd, csu).
> ++subdir-pregen := $(filter mach hurd csu,$(subdirs))
> ++subdir-rest := $(filter-out $(+subdir-pregen),$(subdirs))
> +
> +$(foreach t,$(+ordered_parallel_subdir_targets),$(eval \
> +  $(addsuffix /$(t),$(+subdir-rest)): $(addsuffix /$(t),$(+subdir-pregen))))
> ++subdir-pregen-prev :=
> +$(foreach d,$(+subdir-pregen),$(foreach t,$(+ordered_parallel_subdir_targets),$(eval \
> +  $(d)/$(t): $(addsuffix /$(t),$(+subdir-pregen-prev))))\
> +  $(eval +subdir-pregen-prev := $(d)))
> +# Edges pointing to elf are dropped; the sorted list always forces elf
> +# last, overriding any Depend request, and the elf-last edges below would
> +# otherwise create a cycle.
> +$(foreach t,$(+ordered_parallel_subdir_targets),$(foreach d,$(+subdir-rest),$(eval \
> +  $(d)/$(t): $(addsuffix /$(t),\
> +	      $(filter-out elf,$(filter $(subdirs),$(subdir-deps-$(d))))))))
> +ifneq (,$(filter elf,$(subdirs)))
> +$(foreach t,$(+ordered_parallel_subdir_targets),$(eval \
> +  elf/$(t): $(addsuffix /$(t),$(filter-out elf,$(subdirs)))))
> +endif
> +
> +# Pass barriers: a subdirectory 'others' build links programs against
> +# the libraries, so the 'lib' pass (including the top-level libc.so
> +# link) must have completed.
> +# 'tests' and 'xtests' additionally require the 'others' pass.  The
> +# testroot used by the container tests performs a full installation in
> +# its recipe, which must not run concurrently with the build passes.
> +$(addsuffix /others,$(subdirs)): lib
> +$(addsuffix /tests,$(subdirs)) $(addsuffix /xtests,$(subdirs)): others
> +$(objpfx)testroot.pristine/install.stamp: | others
> +
> +# Timing-sensitive test runs: the threading tests (nptl/htl) and the realtime
> +# tests (rt) are perturbed by the machine load, so run them after the rest of
> +# the test run has finished and one group at a time.  Those subdirectories
> +# also serialize their own tests (.NOTPARALLEL in their Makefiles).
> +#
> +# This only orders a full-suite run ('make check'/'tests'); a targeted
> +# 'make subdir/tests' is left alone.  And it only orders the test run
> +# (run-built-tests=yes); the "build the tests" pass (run-built-tests=no)
> +# is left fully parallel, so every test program still builds concurrently.
> +ifeq ($(run-built-tests),yes)
> +ifneq (,$(filter tests xtests check xcheck,$(MAKECMDGOALS)))
> ++late-test-subdirs := $(filter nptl htl,$(subdirs)) $(filter rt,$(subdirs))
> ++test-run-prev := \
> +  $(addsuffix /tests,$(filter-out $(+late-test-subdirs),$(subdirs)))
> +$(foreach d,$(+late-test-subdirs),\
> +  $(eval $(d)/tests: $(+test-run-prev))\
> +  $(eval +test-run-prev += $(d)/tests))
> +endif
> +endif
> +
> +ifeq (yes,$(build-shared))
> +# The top-level libc.so and linkobj/libc_pic.a rules list these
> +# subdirectory-built files as prerequisites, but no rule at this level
> +# builds them.  The explicit empty recipe (';') is required, a
> +# prerequisite-only rule would send make on an implicitrule search and
> +# have this level compile them itself with the wrong context.
> +$(elf-objpfx)ld.so $(elf-objpfx)sofini.os $(elf-objpfx)interp.os: \
> +  | elf/subdir_lib ;
> +ifneq (,$(filter sunrpc,$(subdirs)))
> +# Makerules explicit adds librpc_compat_pic.a as a dependency of
> +# libc_pic.a.
> +$(common-objpfx)sunrpc/librpc_compat_pic.a: | sunrpc/subdir_lib ;
> +endif
> +# Hurd sysdedp Makeilfe links libc.so against the lib*user-link.so
> +# objects, built by the %-link.so: %_pic.a pattern rule from archives
> +# that only the mach and hurd sub-makes create.
> +ifneq (,$(filter mach,$(subdirs)))
> +$(common-objpfx)mach/libmachuser_pic.a: | mach/subdir_lib ;
> +endif
> +ifneq (,$(filter hurd,$(subdirs)))
> +$(common-objpfx)hurd/libhurduser_pic.a: | hurd/subdir_lib ;
> +endif
> +endif
> +
> +# The remaining target classes keep the old total order.
> ++subdir-chain-prev :=
> +$(foreach d,$(subdirs),$(foreach t,$(+serial_subdir_targets),$(eval \
> +  $(d)/$(t): $(addsuffix /$(t),$(+subdir-chain-prev))))\
> +  $(eval +subdir-chain-prev := $(d)))
>  
>  # Targets to clean things up to various degrees.
>  
> diff --git a/Makerules b/Makerules
> index 180cbee251a..318a916eef0 100644
> --- a/Makerules
> +++ b/Makerules
> @@ -1184,12 +1184,35 @@ ALL_BUILD_CFLAGS = $(BUILD_CFLAGS) $(BUILD_CPPFLAGS) -D_GNU_SOURCE \
>  		   -DIS_IN_build -include $(common-objpfx)config.h
>  
>  # Support the GNU standard name for this target.
> -.PHONY: check
> +# Special target xcheck runs tests which cannot be run unconditionally;
> +# maintainers should use this target.
> +.PHONY: check xcheck
> +
> +# Building and running a subdirectory's tests are fused in its sub-make,
> +# and run-built-tests is fixed for a make instance, so the only way to
> +# build every test program with the recursion fully parallel while the
> +# run still honors the per-subdirectory .NOTPARALLEL (nptl/htl/rt) and the
> +# run-time ordering is to use two passes.  At the top level, 'make check'
> +# therefore builds the test programs (run-built-tests=no, recursion fully
> +# parallel) and then runs them (run-built-tests=yes).  'make tests' and a
> +# subdirectory's own 'check' stay single pass.

Nit: single-pass.

> +check-twopass :=
> +ifndef subdir
> +ifeq (yes,$(run-built-tests))
> +check-twopass := yes
> +endif
> +endif
> +ifeq (yes,$(check-twopass))
> +check:
> +	$(MAKE) run-built-tests=no  tests
> +	$(MAKE) run-built-tests=yes tests
> +xcheck:
> +	$(MAKE) run-built-tests=no  xtests
> +	$(MAKE) run-built-tests=yes xtests
> +else
>  check: tests
> -# Special target to run tests which cannot be run unconditionally.
> -# Maintainers should use this target.
> -.PHONY: xcheck
>  xcheck: xtests
> +endif
>  
>  # Also handle test inputs in sysdeps.
>  vpath %.input $(sysdirs)
> diff --git a/catgets/Makefile b/catgets/Makefile
> index fbc416731ed..630273ac608 100644
> --- a/catgets/Makefile
> +++ b/catgets/Makefile
> @@ -60,6 +60,14 @@ vpath %.c ../locale/programs
>  
>  include ../Rules
>  
> +# The catalog-generation tests below run gencat under specific locales,
> +# and tst-catgets reads the resulting catalog, so the build must wait for
> +# those locales to be generated.  Without this dependency a parallel build
> +# races catgets against localedata and gencat can run before the locale
> +# exists (it then falls back to C and the test fails).
> +LOCALES := de_DE.ISO-8859-1 hr_HR.ISO-8859-2 ja_JP.SJIS
> +include ../gen-locales.mk
> +
>  $(objpfx)gencat: $(gencat-modules:%=$(objpfx)%.o)
>  
>  catgets-CPPFLAGS := -DNLSPATH='"$(localedir)/%L/%N:$(localedir)/%L/LC_MESSAGES/%N:$(localedir)/%l/%N:$(localedir)/%l/LC_MESSAGES/%N:"'
> @@ -95,7 +103,7 @@ tst-catgets-ENV = NLSPATH="$(objpfx)%l/%N.cat" LANG=de \
>  ifeq ($(run-built-tests),yes)
>  # This test just checks whether the program produces any error or not.
>  # The result is not tested.
> -$(objpfx)test1.cat: test1.msg $(objpfx)gencat
> +$(objpfx)test1.cat: test1.msg $(objpfx)gencat $(gen-locales)
>  	$(built-program-cmd-before-env) \
>  	$(run-program-env) LC_ALL=hr_HR.ISO-8859-2 \
>  	$(built-program-cmd-after-env) -H $(objpfx)test1.h $@ $<; \
> @@ -103,7 +111,7 @@ $(objpfx)test1.cat: test1.msg $(objpfx)gencat
>  $(objpfx)test2.cat: test2.msg $(objpfx)gencat
>  	$(built-program-cmd) -H $(objpfx)test2.h $@ $<; \
>  	$(evaluate-test)
> -$(objpfx)de/libc.cat: $(objpfx)de.msg $(objpfx)gencat
> +$(objpfx)de/libc.cat: $(objpfx)de.msg $(objpfx)gencat $(gen-locales)
>  	$(make-target-directory)
>  	$(built-program-cmd-before-env) \
>  	$(run-program-env) LC_ALL=de_DE.ISO-8859-1 \
> @@ -116,7 +124,7 @@ $(objpfx)de.msg: xopen-msg.awk $(..)po/de.po
>  	LC_ALL=C $(AWK) -f $^ $< > $@
>  
>  $(objpfx)test-gencat.out: test-gencat.sh $(objpfx)test-gencat \
> -			  $(objpfx)sample.SJIS.cat
> +			  $(objpfx)sample.SJIS.cat $(gen-locales)
>  	$(SHELL) $< $(common-objpfx) '$(test-program-cmd-before-env)' \
>  		 '$(run-program-env)' '$(test-program-cmd-after-env)'; \
>  	$(evaluate-test)
> diff --git a/elf/Makefile b/elf/Makefile
> index 5ede78c5902..5598bafba76 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -1436,6 +1436,7 @@ ifndef avoid-generated
>  # Makefile fragment to be included.
>  define include_dsosort_tests
>  $(objpfx)$(1).generated-makefile: $(1)
> +	$$(make-target-directory)
>  	$(PYTHON) $(..)scripts/dso-ordering-test.py \
>  	--description-file $$< --objpfx $(objpfx) --output-makefile $$@T
>  	mv $$@T $$@
> @@ -1444,6 +1445,7 @@ endef
>  # Likewise, where the .def file itself is generated.
>  define include_dsosort_tests_objpfx
>  $(objpfx)$(1).generated-makefile: $(objpfx)$(1)
> +	$$(make-target-directory)
>  	$(PYTHON) $(..)scripts/dso-ordering-test.py \
>  	--description-file $$< --objpfx $(objpfx) --output-makefile $$@T
>  	mv $$@T $$@
> @@ -1462,12 +1464,15 @@ $(eval $(call include_dsosort_tests,dso-sort-tests-1.def))
>  $(eval $(call include_dsosort_tests,dso-sort-tests-2.def))
>  
>  $(objpfx)dso-sort-tests-all2.def: dso-sort-tests-all.py
> +	$(make-target-directory)
>  	$(PYTHON) $< 2 > $@
>  
>  $(objpfx)dso-sort-tests-all3.def: dso-sort-tests-all.py
> +	$(make-target-directory)
>  	$(PYTHON) $< 3 > $@
>  
>  $(objpfx)dso-sort-tests-all4.def: dso-sort-tests-all.py
> +	$(make-target-directory)
>  	$(PYTHON) $< 4 > $@
>  
>  $(eval $(call include_dsosort_tests_objpfx,dso-sort-tests-all2.def))
> diff --git a/htl/Makefile b/htl/Makefile
> index 5bad0333a5a..0e5a8b8f689 100644
> --- a/htl/Makefile
> +++ b/htl/Makefile
> @@ -257,3 +257,10 @@ $(addprefix $(objpfx),$(tests-static) $(xtests-static)): $(srcdir)/libpthread_sy
>  else
>  $(addprefix $(objpfx),$(tests) $(test-srcs)): $(srcdir)/libpthread_syms.a $(objpfx)libpthread.a
>  endif
> +
> +# The tests here better do not run in parallel.

Say something like: "Like rt, these tests prefer to be run serially."

> +ifeq ($(run-built-tests),yes)
> +ifneq ($(filter %tests,$(MAKECMDGOALS)),)
> +.NOTPARALLEL:
> +endif
> +endif
> diff --git a/rt/Makefile b/rt/Makefile
> index 39a3e5620b3..24e43a87599 100644
> --- a/rt/Makefile
> +++ b/rt/Makefile
> @@ -109,3 +109,11 @@ LDFLAGS-rt.so = -Wl,--enable-new-dtags,-z,nodelete
>  $(objpfx)librt.so: $(shared-thread-library)
>  
>  tst-mqueue7-ARGS = -- $(host-test-program-cmd)
> +
> +# The timer and message-queue tests here are timing-sensitive and better
> +# do not run in parallel.
> +ifeq ($(run-built-tests),yes)
> +ifneq ($(filter %tests,$(MAKECMDGOALS)),)
> +.NOTPARALLEL:
> +endif
> +endif

Reviewed-by: Sam James <[email protected]>

The changes look good. Also, when testing, thus far I only had one small
issue:
https://inbox.sourceware.org/libc-alpha/822344f7cd03b8f199d0e612868221d13d14300d.1783035357.git.sam@gentoo.org/

Thanks.
signature.asc (application/pgp-signature, 418 B)
-----BEGIN PGP SIGNATURE-----

iQEBBAEWCgCpFiEEJaa7iN2bdkxrVUHCc4QJ9SDfkZAFAmpHlZcbFIAAAAAABAAO
bWFudTIsMi41KzEuMTIsMiwyXxSAAAAAAC4AKGlzc3Vlci1mcHJAbm90YXRpb25z
Lm9wZW5wZ3AuZmlmdGhob3JzZW1hbi5uZXQyNUE2QkI4OEREOUI3NjRDNkI1NTQx
QzI3Mzg0MDlGNTIwREY5MTkwDxxzYW1AZ2VudG9vLm9yZwAKCRBzhAn1IN+RkITB
AP9vBXkKFlgJ40qe7mQ+3fyiVTo9BbcX9s+Y8s1ggEudpAEA8V7/L8DTMMJmb93e
NJ5F/1QtLdQlJk1bhHeQ32/wxAw=
=8SSs
-----END PGP SIGNATURE-----
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.