+cc Petr

FYI that were some discussion on this in the v2 thread at [0], which will result
in some changes for v4.

[0]:https://lore.kernel.org/all/arEZrFmXeCO4hZpP@gremlin

On Thu, Sep 17, 2026 at 05:06:24PM +0100, Lorenzo Stoakes (ARM) wrote:
> With the .mod.S change in place, module finalisation on allmodconfig builds
> consists of a large number of very short-lived jobs.
>
> For allmodconfig x86-64 this can be on the order of ~22,000 jobs of a few
> milliseconds in duration each.
>
> Each job entails processing ~22k .cmd files, so the combination of heavy
> overhead and small individual job results in a lot of unnecessary and
> repeated work even with all cores being utilised.
>
> The solution is to batch by a number of jobs. Determining which value makes
> sense was done empirically.
>
> On a 128-thread threadripper box doing an allmodconfig build, best of
> 2, *.ko, *.mod.o deleted each time:
>
>     modules per chunk    instances    wall
>     -----------------    ---------    ------
>                     1        11171    10.22s
>                     2         5586     6.72s
>                     4         2793     4.97s
>                     8         1397     4.26s
>                    16          699     4.05s
>                    32          350     4.05s
>                    64          175     4.07s
>                   128           88     4.05s
>                   256           44     4.03s
>                   512           22     4.18s
>
> Wall time flattens for 16-256 module batches.
>
> A slower/lower core machine will do better with fewer modules-per-batch, a
> faster/higher core machine will do better with more modules-per-batch.
>
> Therefore, take the midpoint which works in the most margin in either
> direction - 128 modules per batch.
>
> This naturally scales with module count too as the optimum gains are
> obtained with higher module count, so fewer batches in this case costs less
> overhead.
>
> Each instance holds only its own modules' variables and the top-level one
> reads no per-module .cmd files at all, the same rules serve both levels,
> and an instance is told its chunk with modfinal-first=<index>.
>
> Only the top-level instance builds .module-common.o, to the chunks it is a
> plain prerequisite, so no two instances ever write it.
>
> "make modules" with every *.mod.o and *.ko deleted goes from 28.9s to 15.9s
> with clang 22. No-op "make modules" goes from 5.6s to 4.8s, as checking the
> 22,000 targets is spread over the chunks too.
>
> Whole build, 128-thread Threadripper 9980X, best of N runs:
>
>                                          before   after     delta
>                                          -------------------------------
>   x86 allmodconfig, no-op make, gcc         2.3s     1.4s    -0.85s (-38%)
>   x86 allmodconfig, no-op make, clang       2.8s     1.8s    -0.94s (-34%)
>   x86 allmodconfig, clean, gcc            306.6s   294.1s     -12.6s (-4%)
>   x86 allmodconfig, clean, clang          290.2s   283.9s     -6.4s (-2%)
>
> Assisted-by: LLM
> Signed-off-by: Lorenzo Stoakes (ARM) <[email protected]>
> ---
>  scripts/Makefile.modfinal | 30 +++++++++++++++++++++++++++++-
>  1 file changed, 29 insertions(+), 1 deletion(-)
>
> diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
> index 75e9effdf02c..4d5e6e1a0ff9 100644
> --- a/scripts/Makefile.modfinal
> +++ b/scripts/Makefile.modfinal
> @@ -13,9 +13,30 @@ include $(srctree)/scripts/Makefile.lib
>  # find all modules listed in modules.order
>  modules := $(call read-file, modules.order)
>
> +modfinal-chunk-size := 128
> +
> +ifdef modfinal-first
> +
> +# this instance handles the chunk of modules.order starting at 
> $(modfinal-first)
> +modules := $(wordlist $(modfinal-first), $(words $(modules)), $(modules))
> +modules := $(wordlist 1, $(modfinal-chunk-size), $(modules))
> +
>  __modfinal: $(modules:%.o=%.ko)
>       @:
>
> +else
> +
> +modfinal-chunks := $(addprefix chunk-, $(shell seq 1 $(modfinal-chunk-size) 
> $(words $(modules))))
> +
> +PHONY += $(modfinal-chunks)
> +$(modfinal-chunks): .module-common.o
> +     $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modfinal 
> modfinal-first=$(@:chunk-%=%)
> +
> +__modfinal: $(modfinal-chunks)
> +     @:
> +
> +endif
> +
>  # modname and part-of-module are set to make c_flags define proper module 
> flags
>  modname = $(notdir $(@:.mod.o=))
>  part-of-module = y
> @@ -29,8 +50,11 @@ quiet_cmd_as_mod_o = AS [M]  $@
>  %.mod.o: %.mod.S FORCE
>       $(call if_changed,as_mod_o)
>
> +# Built by the top-level instance alone, the chunks take it as a plain file.
> +ifndef modfinal-first
>  .module-common.o: $(srctree)/scripts/module-common.c FORCE
>       $(call if_changed_rule,cc_o_c)
> +endif
>
>  ifneq ($(WARN_ON_UNUSED_TRACEPOINTS),)
>  cmd_check_tracepoint = $(objtree)/scripts/tracepoint-update --module $<;
> @@ -58,7 +82,11 @@ ifdef CONFIG_DEBUG_INFO_BTF_MODULES
>  endif
>       +$(call cmd,check_tracepoint)
>
> -targets += $(modules:%.o=%.ko) $(modules:%.o=%.mod.o) .module-common.o
> +ifdef modfinal-first
> +targets += $(modules:%.o=%.ko) $(modules:%.o=%.mod.o)
> +else
> +targets += .module-common.o
> +endif
>
>  # Add FORCE to the prerequisites of a target to force it to be always 
> rebuilt.
>  # ---------------------------------------------------------------------------
>
> --
> 2.55.0
>

--
Cheers, Lorenzo

Reply via email to