Извините, но в 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/
Добавить комментарий