Как стать автором
Обновить

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

Время на прочтение4 мин
Количество просмотров1.7K
Автор оригинала: Tavian Barnes

Извините, но в 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% одного ядра — это уже не смешно, это настоящее ускорение.

Теги:
Хабы:
+11
Комментарии14

Публикации

Работа

QT разработчик
4 вакансии
Программист C++
93 вакансии

Ближайшие события