Параллельный ./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% одного ядра — это уже не смешно, это настоящее ускорение.

@gxcreator
26.04.2025 18:30 UTC
Первоисточник

Комментарии

@kovserg
26.04.2025 14:09 UTC
+3

"Извините, но в 2025 году" что мешает использовать базы данных для подобных задач? Почему информацию о системе и подсистемах на складывать в базу данных, а не проверять 100500 раз на каждый чих?

@gxcreator
26.04.2025 15:54 UTC
0

Хороший вопрос, думаю так просто исторически сложилось. Складывание в базу данных тоже имеет свои проблемы, но то, как autotools работает сейчас это топорно, медленно и неудобно.

27.04.2025 05:25 UTC
0

то, как autotools работает сейчас

Дайте стюардессе доработать до пенсии и закопайте ;)

@Sadler
26.04.2025 16:02 UTC
0

Актуализация данных в БД будет заведомо более хрупкой, нежели фактическая проверка. Можно сделать это в виде кэша с ограниченным временем жизни, но это не отменяет необходимости параллельного запуска, даже если такой запуск будет реже.

С другой стороны, параллелизм создаёт массу новых проблем: недопустимо, если последовательное и параллельное выполнение теста будет давать разные результаты. Если это оставить на откуп разработчика, то этим никто не станет заниматься, всё будет по принципу "и так сойдёт", иногда вызывая дополнительные неясные ошибки при сборке.

Значит, чтобы это работало хорошо, необходимо централизованно писать наборы стандартных тестов различных библиотек и подсистем так, чтобы они были заведомо независимы и потокобезопасны, и спускать эти готовые тесты пользователям в неизменном виде, а нестандартные пусть выполняются последовательно. Такие тесты желательно писать самим разработчикам библиотек/приложений, с которыми производится взаимодействие, т.е. необходимо в репозитории иметь какой-то набор пакетов с такими тестами, которые будут мейнтейниться разработчиками.

Решение интересное, но чертовски сложное в организации социально.

@apevzner
26.04.2025 17:17 UTC
0

pkg-config?

@DjUmnik
26.04.2025 14:41 UTC
0

Да уж. Как же это медленно и хрупко. Сборка C/C++ библиотек и зависимостей это всегда лотерея - никогда не знаешь, что сломается на этот раз. Я пользуюсь Conan, и он, помимо решения проблем, создает новые.

@Gorthauer87
26.04.2025 16:48 UTC
+1

Кажется, в 25 году пора просто вырезать это все и переходить на что нибудь более вменяемое. Думаю, что на древние юниксы можно уже и забить.

@apevzner
26.04.2025 17:17 UTC
+2

./configure был нужен, когда UNIX существовал в 100500-х вариантах, и задача была собрать данный пакет на данной системе хоть как-то, приспособившись к особенностям системы.

Сейчас система примерно одна, и задача заключается скорее не в том, чтобы приспособиться к ее особенностям, а в том, чтобы получить на разных инсталляциях системы более-менее одинаковый результат, понятным человеку образом затребовав недостающие ресурсы для её достижения.

Это совсем не то, чем занимается /configure.

P.S. Кстати, есть еще такая полезная штука, как pkg-config...

@rukhi7
26.04.2025 19:18 UTC
0

"Извините, но в 2025 году" можно уже не запускать конфиг перед каждой компиляцией...

Или нельзя? (в вашем случае)

@gxcreator
26.04.2025 20:57 UTC
0

Скажем, если собирать образ образ какой-нибудь ОС/контейнера/сдк с зависимостями, для каждого пакета будет дергается довольно медленный configure. В итоге 50+ процентов времени сборки потратится на то, чтобы гонять одинаковые тесты в один потом для каждого пакета.

27.04.2025 07:57 UTC
+1

можно пакеты по зависимостям одного уровня собирать параллельно, конечно это не поможет там где можно:

просто уменьшил количество ненужных проверок

в общем как всегда, все, к сожалению, сводится к тому что все надо просто переписать...

Но, как известно (хотя мало кто об этом помнит), софт придумали именно для того чтобы его можно было просто переписать!

@Sleuthhound
27.04.2025 07:02 UTC
+2

Что только люди не делают, лишь бы не пользоваться cmake в 21 веке

@randomsimplenumber
27.04.2025 08:33 UTC
0

Не хотят переписывать.

@svistkovr
27.04.2025 08:10 UTC
+1

Если бы проблема была только в конфигурации сборки. Большую часть времени занимает сама компиляция, особенно если надо раскрыть все макросы.
Вот вам парочку советов из Gentoo сообщества:
1) создаем виртуальный диск в оперативной памяти tmpfs и компилим на нём.
2) ccache для кеширования сборки
3) distcc для распределённой компиляции

@mpa4b
05.05.2025 08:32 UTC
0

1) создаем виртуальный диск в оперативной памяти tmpfs и компилим на нём.

Много чего компилировал и компилирую, в т.ч. и в генте, но не помню случая, чтобы когда-то компиляция рогом упиралась бы в диск. Ну, конечно, замена хдд на ссд даст наверное 5-10% выигрыша по времени, но не более того (компиляция во все 32 потока на ryzen'е). Потому мне всегда странны рассказы о том, что "заменил диск -- стало в 2 раза быстрее компилироваться". Это наверное не про c/c++ типичные софты, как в линуксах.

05.05.2025 09:11 UTC
0

причем здесь жесткий диск. вы не внимательно читаете.
не диск поменять, а создать раздел диска в оперативной памяти. скорость работы с файлами в оперативной памяти больше чем на физическом диске.

05.05.2025 09:39 UTC
0

Если замена хдд, который давал условно 100иопсов и 100 мегабайт в секунду на ссд, где тыщи иопсов и гигабайт в секунду даёт выигрыш хорошо если в 10%, то дальнейнее ускорение рамдиском, где иопсы и бендвиз ограничены скоростью чтения и копирования у процессора, вряд ли магически ускорит хотя бы в 2 раза что-то. C рамдисками я тоже экспериментировал, и ускорение там относительно ssd получалось для компиляции ещё меньше, чем от hdd->ssd.

Мой посыл был о том, что в задачи типичного компилирования c/c++ диск -- самое последнее место, в которое машина упрётся по ограничению скорости компиляции.