Параллельный ./configure

от автора

Извините, но в 2025 году — это просто смешно:

$ time ./configure ... ./configure  13.80s user 12.72s system 69% cpu 38.018 total $ time make -j48 ... make -j48  12.05s user 4.70s system 593% cpu 2.822 total

Я заплатил приличные деньги за 24 ядра CPU, а ./configure умудряется грузить только 69% одного ядра! В результате этот рандомный проект конфигурится в 13.5 раз медленнее, чем потом реально собирается.

Назначение ./configure — это просто много раз вызвать компилятор и проверить, какие тесты прошли. Типа: есть ли нужные заголовки, функции, структуры, поля — чтобы писать переносимый код. Эта задача идеально параллелится, но автотулы (autoconf, cmake, meson и прочие) до сих пор не умеют это делать.

Типичная структура configure скрипта выглядит так:

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 ... 

То есть проверки идут последовательно. Хотя на практике их спокойно можно было бы гнать параллельно. Более того, инструмент для параллельного исполнения у нас уже есть — make!

Почему бы не использовать его? Идея простая: У нас будет специальный configure.mk, который будет генерить Makefile и config.h через make -j:

# configure.mk # The default goal generates both outputs, and merges the logs together config: Makefile config.h     cat Makefile.log config.h.log >$@.log     rm Makefile.log config.h.log

Проверки превращаются в независимые таргеты. Например:

# configure.mk  # Дефолтные значения на всякий случай: CC ?= cc CPPFLAGS ?= -D_GNU_SOURCE CFLAGS ?= -g LDFLAGS ?=  # Экспортируем их, чтобы избежать удаления обратных слешей: export _CC=${CC} export _CPPFLAGS=${CPPFLAGS} export _CFLAGS=${CFLAGS} export _LDFLAGS=${LDFLAGS}  #Генерируем Makefile: Makefile:     printf 'CC := %s\n' "$$_CC" >$@     printf 'CPPFLAGS := %s\n' "$$_CPPFLAGS" >>$@     printf 'CFLAGS := %s\n' "$$_CFLAGS" >>$@     printf 'LDFLAGS := %s\n' "$$_LDFLAGS" >>$@ 

Экспортирование export сделано так, чтобы избежать удаления обратных слешей из вызовов типа таких:

$ ./configure CPPFLAGS='-DMACRO=\"string\"'

Теперь проверим поддержку флагов (-Wall, -pthread, и т.д.) с помощью небольшого скрипта flags.sh:

#!/bin/sh  set -eu  VAR="$1" FLAGS="$2" shift 2  if "$@" $FLAGS; then     printf '%s += %s\n' "$VAR" "$FLAGS" fi 

Простой пример:

$ ./flags.sh CFLAGS -Wall cc empty.c  CFLAGS += -Wall

Скрипт выведет CFLAGS += -Wall только если cc empty.c -Wall завершится успешно.

Мы можем использовать такой подход для генерации некоторых фрагментов makefile, которые включают только поддерживаемые флаги.

ALL_FLAGS = ${CPPFLAGS} ${CFLAGS} ${LDFLAGS}  # Запускаем компилятор с заданными флагами, отправляя # # - stdout в foo.mk (напр. CFLAGS += -flag) # - stderr в foo.mk.log (напр. error: unrecognized command-line option ‘-flag’) # - бинарники в foo.mk.out #   - а потом сразу их удаляем TRY_CC = ${CC} ${ALL_FLAGS} empty.c -o $@.out >$@ 2>$@.log && rm -f $@.out $@.d  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}

Каждый из этих таргетов генерит крошечный фрагмент мэйкфайла, отвечающий за один флаг и каждый из них может работать независимо, параллельно!

Как только тесты будут готовы, мы можем объединить их все в основной файл Makefile и очистить мусор:

FLAGS := \     deps.mk \     Wall.mk \     pthread.mk \     bind-now.mk  Makefile: ${FLAGS}     printf 'CC := %s\n' "$$_CC" >$@     ...     cat ${FLAGS} >>$@     cat ${FLAGS:%=%.log} >$@.log     rm ${FLAGS} ${FLAGS:%=%.log}

Осталось добавить в Makefile ту часть которая фактически собирает наше приложение. Мы можем написать простой main.mk следующим образом:

#main.mk OBJS := main.o  app: ${OBJS}     ${CC} ${CFLAGS} ${LDFLAGS} ${OBJS} -o $@  ${OBJS}:     ${CC} ${CPPFLAGS} ${CFLAGS} -c ${@:.o=.c} -o $@  -include ${OBJS:.o=.d}

А затем добавить его в Makefile после всех флагов:

Makefile: ${FLAGS}     ...     cat main.mk >>$@

Ещё нам нужно сгенерировать config.h, который определяет макросы, сообщающие нам, существуют ли определенные библиотеки/заголовки/функции/поля структур и т.д.

Проверка фичи делается тоже через компиляцию микро-программок, например:

проверка statx() have_statx.c :

#include <fcntl.h> #include <sys/stat.h>  int main(void) {     struct statx stx;     return statx(AT_FDCWD, ".", 0, STATX_BTIME, &stx); }

проверка st_birthtim() have_st_birthtim.c :

#include <sys/stat.h>  int main(void) {     struct stat sb = {0};     return sb.st_birthtim.tv_sec; }

А 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

который сгенерирует что-то типа в зависимости от результата выполнения:

#define HAVE_STATX 1 #define HAVE_ST_BIRTHTIM 0

Мы можем использовать его в makefile вот так:

#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 $@.out >$@ 2>$@.log     rm -f $@.out $@.d 

И потом всё это склеить в config.h с защитой от двойного включения вот так:

#header.mk config.h: ${HEADERS}     printf '#ifndef CONFIG_H\n' >$@     printf '#define CONFIG_H\n' >>$@     cat ${HEADERS} >>$@     printf '#endif\n' >>$@     cat ${HEADERS:%=%.log} >$@.log     rm ${HEADERS} ${HEADERS:%=%.log}

В итоге, полноценный ./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 "$@"

Я сделал рабочий пример на GitHub где все эти файлы представлены полностью — можете скопировать себе. Демо печатает время создания файла, если оно поймет как делать это на вашей системе.

Я также давно использую подобную сборку в своем проекте bfs, и разница в производительности колоссальная:

$ time ./configure ... ./configure  1.44s user 1.78s system 802% cpu 0.401 total  $ time make -j48 ... make -j48  1.89s user 0.64s system 817% cpu 0.310 total 

Конечно, часть выигрыша приходит от того, что я просто уменьшил количество ненужных проверок, но загрузка CPU на 802% вместо 69% одного ядра — это уже не смешно, это настоящее ускорение.


ссылка на оригинал статьи https://habr.com/ru/articles/904664/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *