Привет, Хабр!

Обычно я за то, чтобы не усложнять рантайм лишний раз, но здесь случай приятный: можно включить у своих CLI автоматический выбор библиотек под реальные возможности CPU и не ломать обратную совместимость. Механизм называется glibc-hwcaps, появился в glibc 2.33, и он позволяет динамическому загрузчику подбирать lib’ы из специальных поддиректорий по уровню x86-64-v2/v3/v4. Это ровно те уровни, что определены в x86-64 psABI, без частных расширений конкретных вендоров. На практике достаточно положить собранный под v3 lib рядом с обычным, соблюсти SONAME, и ld.so сам подхватит оптимизированный вариант на новых процессорах. На старом железе всё продолжит работать на базовой сборке.

Коротко про уровни v2/v3/v4 и как понять, что поддерживается

Уровни введены совместно AMD, Intel, Red Hat и SUSE. v2 закрепляет набор вроде SSE4.2, SSSE3, POPCNT, v3 добавляет AVX, AVX2, BMI1/2, FMA и прочие массовые расширения, v4 — это уже AVX-512 семейство. Компиляторы понимают их напрямую через -march=x86-64-v2/v3/v4.

Проверим, у динамического линковщика есть подсказка в --help. Выполните

# Fedora, openSUSE, многие дистрибутивы:
 /usr/lib64/ld-linux-x86-64.so.2 --help | sed -n '/glibc-hwcaps/,/Subdirectories/p'
# Debian/Ubuntu обычно:
 /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 --help | sed -n '/glibc-hwcaps/,/Subdirectories/p'

Внизу увидите блок вроде:

Subdirectories of glibc-hwcaps directories, in priority order:
  x86-64-v4
  x86-64-v3 (supported, searched)
  x86-64-v2 (supported, searched)

Если у вас supported стоит у v3 — можно смело отдавать оптимизированные библиотеки под AVX2. Если у v4 нет «supported», значит AVX-512 недоступен.

Где будут лежать оптимизированные библиотеки

Загрузчик ищет библиотеки не только в стандартных путях, но и в подкаталогах glibc-hwcaps. Типичные пути:

  • Debian и производные: /usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/

  • Fedora, openSUSE: /usr/lib64/glibc-hwcaps/x86-64-v3/

Файл имеет тот же SONAME, что и базовый, пример: /usr/lib64/libfoo.so.1 и /usr/lib64/glibc-hwcaps/x86-64-v3/libfoo.so.1. Загрузчик сам выберет наиболее подходящий.

Включаем v3 для своего CLI через библиотеки

Самый практичный путь — не гоняться за отдельным бинарём под v3, а положить критичные по CPU зависимости в glibc-hwcaps. Так вы сохраняете один CLI бинарь под baseline и раздаёте ускоренные lib’ы для новых CPU. Дистрибутивы уже пошли этим путём и массово копируют библиотеки в эти overlay-подкаталоги.

Скетч процесса для проектной библиотеки libfastsum:

# 1) Библиотека: baseline
cc -O3 -fPIC -shared -o libfastsum.so.1.0 fastsum.c \
   -Wl,-soname,libfastsum.so.1

# 2) Библиотека: v3
cc -O3 -fPIC -shared -o libfastsum.so.1.0.v3 fastsum.c \
   -march=x86-64-v3 -mtune=generic -Wl,-soname,libfastsum.so.1

# 3) Установка
install -D -m 0755 libfastsum.so.1.0        /usr/lib64/libfastsum.so.1.0
ln -sf libfastsum.so.1.0                    /usr/lib64/libfastsum.so.1

install -D -m 0755 libfastsum.so.1.0.v3 \
  /usr/lib64/glibc-hwcaps/x86-64-v3/libfastsum.so.1.0
ln -sf ../../libfastsum.so.1.0 \
  /usr/lib64/glibc-hwcaps/x86-64-v3/libfastsum.so.1

Проверяем выбор версий:

# Просим детальный лог выбора библиотек
LD_DEBUG=libs ./your_cli 2>&1 | grep fastsum

Вы увидите, откуда подхватилось libfastsum.so.1.

Пакуем это в .deb и .rpm

Debian-скетч через debian/*.install:

# debian/libfastsum.install
usr/lib/x86_64-linux-gnu/libfastsum.so.1.*
usr/lib/x86_64-linux-gnu/libfastsum.so.1

# debian/libfastsum-x86-64-v3.install
usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libfastsum.so.1.*
usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libfastsum.so.1

И метапакет вашего приложения с зависимостью от libfastsum как от обычной библиотеки. Аналогично для RPM: используем %{_libdir}/glibc-hwcaps/x86-64-v3/ и отделите сабпакет -x86-64-v3. openSUSE имеет готовые паттерны для установки оптимизированных сабпакетов, можно ориентироваться на их подход.

Что делать с libcrypto под v3 и v4

OpenSSL уже содержит собственный runtime-dispatch по CPUID, но glibc-hwcaps даёт ещё один уровень свободы: можно собирать libcrypto.so и libssl.so под v3/v4 и класть в overlay-каталоги. Так делают в openSUSE: там реально встречаются .../glibc-hwcaps/x86-64-v3/libssl.so.* и связанные сабпакеты. Если версия OpenSSL меняется, следите за соответствием SONAME и символ-версий. Держать v3-сборки в отдельном сабпакете, который рекомендуется на совместимых системах, удобно и безопасно.

Набросок сборки:

# Пример: OpenSSL 3.x с -march=x86-64-v3
./Configure linux-x86_64 enable-ec_nistp_64_gcc_128 \
    -march=x86-64-v3 -mtune=generic -O3 -fno-plt \
    --prefix=/usr --libdir=/usr/lib64
make -j
DESTDIR=/tmp/pfx make install_sw

# Разложить артефакты:
install -D -m 0755 /tmp/pfx/usr/lib64/libcrypto.so.3 \
  /usr/lib64/glibc-hwcaps/x86-64-v3/libcrypto.so.3
install -D -m 0755 /tmp/pfx/usr/lib64/libssl.so.3 \
  /usr/lib64/glibc-hwcaps/x86-64-v3/libssl.so.3

Проверяем ускорение:

# Отдельно меряем v2 vs v3 с одним бинарём:
openssl speed -evp sha256

Если сомневаетесь, что берётся нужная библиотека, включаемLD_DEBUG=libs.

Что с libm

libm — часть glibc, и многие функции уже имеют IFUNC-диспетчеризацию, выбирающую AVX2 и AVX-512 варианты на лету. Подменять libm.so.6 своей сборкой я не рекомендую: это риск непредсказуемых несовместимостей. Лучше убедиться, что ваша система обновлена до свежей glibc, где уже есть векторные реализации, и для экспериментов использовать механизм GLIBC_TUNABLES, чтобы имитировать отключение тех или иных расширений и увидеть разницу.

Проверить, что IFUNC-варианты есть, можно так:

objdump -T /usr/lib64/libm.so.6 | grep -E 'sin|cos|exp' | head

Если задача всё же требует альтернативной libm, рассмотрите специализированные векторные math-библиотеки вроде SLEEF, но устанавливайте их под другим именем и линковкой на стороне вашего приложения. Не подменяйте системную libm. Это технически возможно, но не нужно.

GLIBC_TUNABLES

Tunables — это настройка поведения glibc через переменную окружения GLIBC_TUNABLES=name=value[:name=value...]. Интерфейс гибкий, но не является стабильной частью ABI, то есть состав тьюнаблов может меняться между версиями.

Полезные для x86 примеры: пороги для rep movsb/stosb, пороги non-temporal store и, главное, маскирование аппаратных возможностей, чтобы отключить AVX2 или конкретные маркеры и посмотреть, как это повлияет на ваш кейс.

Примеры, с которыми можно играться:

# Отключить использование AVX2 реализаций в glibc:
GLIBC_TUNABLES="glibc.cpu.hwcaps=-AVX2_Usable" ./your_cli

# Иногда в старых версиях тьюнабл назывался glibc.tune.hwcaps:
GLIBC_TUNABLES="glibc.tune.hwcaps=-AVX2_Usable" ./your_cli

# Подкрутить порог, с которого glibc начинает использовать rep movsb:
GLIBC_TUNABLES="glibc.cpu.x86_rep_movsb_threshold=1024" ./your_cli

# Подвинуть порог non-temporal stores в memset/memmove:
GLIBC_TUNABLES="glibc.cpu.x86_non_temporal_threshold=65536:glibc.cpu.x86_memset_non_temporal_threshold=65536" ./your_cli

Первый и второй примеры пригодятся, если хотите принудительно выключить AVX2 и посмотреть, как изменится производительность. Третий и четвёртый — если у вас характерный профиль копирования больших буферов, и хочется подобрать пороги. Конкретные имена тьюнаблов и доступность зависят от версии glibc

Замечание по безопасности. В 2023 году была история с CVE-2023-4911, когда обработка GLIBC_TUNABLES в динамическом загрузчике оказалась уязвимой на некоторых конфигурациях. Патчи давно вендорены и доставлены. Но общее правило простое: не передавайте неподконтрольные значения GLIBC_TUNABLES в окружении привилегированных программ и образов.

Мини-бенч: AVX2 и AVX-512

Симулируем сценарий: CLI дергает библиотечную функцию, манипулирующую большими буферами. Сделаем простую библиотеку libvecops с версией под baseline и под v3 и померяем memcpy-heavy путь. Плюс дадим опцию сравнить AVX-512, если CPU умеет.

vecops.h:

#pragma once
#include <stddef.h>
void vec_add_u8(unsigned char* __restrict dst,
                const unsigned char* __restrict a,
                const unsigned char* __restrict b,
                size_t n);

vecops_baseline.c:

#include "vecops.h"
void vec_add_u8(unsigned char* __restrict dst,
                const unsigned char* __restrict a,
                const unsigned char* __restrict b,
                size_t n) {
    for (size_t i = 0; i < n; i++) dst[i] = a[i] + b[i];
}

vecops_avx2.c:

#include "vecops.h"
#include <immintrin.h>
void vec_add_u8(unsigned char* __restrict dst,
                const unsigned char* __restrict a,
                const unsigned char* __restrict b,
                size_t n) {
    size_t i = 0;
    for (; i + 32 <= n; i += 32) {
        __m256i va = _mm256_loadu_si256((const __m256i*)(a + i));
        __m256i vb = _mm256_loadu_si256((const __m256i*)(b + i));
        __m256i vd = _mm256_adds_epu8(va, vb);
        _mm256_storeu_si256((__m256i*)(dst + i), vd);
    }
    for (; i < n; i++) dst[i] = a[i] + b[i];
}

vecops_avx512.c:

#include "vecops.h"
#include <immintrin.h>
void vec_add_u8(unsigned char* __restrict dst,
                const unsigned char* __restrict a,
                const unsigned char* __restrict b,
                size_t n) {
#ifdef __AVX512BW__
    size_t i = 0;
    for (; i + 64 <= n; i += 64) {
        __m512i va = _mm512_loadu_si512((const void*)(a + i));
        __m512i vb = _mm512_loadu_si512((const void*)(b + i));
        __m512i vd = _mm512_adds_epu8(va, vb);
        _mm512_storeu_si512((void*)(dst + i), vd);
    }
    for (; i < n; i++) dst[i] = a[i] + b[i];
#else
    // если нет AVX512BW при сборке, fallback сделает линкер
    for (size_t i = 0; i < n; i++) dst[i] = a[i] + b[i];
#endif
}

Сборка и раскладка:

# baseline
cc -O3 -fPIC -shared -o libvecops.so.1.0 vecops_baseline.c -Wl,-soname,libvecops.so.1
install -D -m 0755 libvecops.so.1.0 /usr/lib64/libvecops.so.1.0
ln -sf libvecops.so.1.0 /usr/lib64/libvecops.so.1

# AVX2 под v3
cc -O3 -march=x86-64-v3 -mtune=generic -fPIC -shared \
   -o libvecops.so.1.0.v3 vecops_avx2.c -Wl,-soname,libvecops.so.1
install -D -m 0755 libvecops.so.1.0.v3 \
  /usr/lib64/glibc-hwcaps/x86-64-v3/libvecops.so.1.0
ln -sf ../../libvecops.so.1.0 \
  /usr/lib64/glibc-hwcaps/x86-64-v3/libvecops.so.1

# AVX-512 под v4 (если есть такое железо)
cc -O3 -march=x86-64-v4 -mtune=generic -fPIC -shared \
   -o libvecops.so.1.0.v4 vecops_avx512.c -Wl,-soname,libvecops.so.1
install -D -m 0755 libvecops.so.1.0.v4 \
  /usr/lib64/glibc-hwcaps/x86-64-v4/libvecops.so.1.0
ln -sf ../../libvecops.so.1.0 \
  /usr/lib64/glibc-hwcaps/x86-64-v4/libvecops.so.1

Мини-бенч:

// bench.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "vecops.h"

static double sec(clockid_t clk, struct timespec t0, struct timespec t1) {
    return (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
}

int main(int argc, char** argv) {
    size_t n = argc > 1 ? strtoull(argv[1], NULL, 10) : 64*1024*1024;
    unsigned char* a = aligned_alloc(64, n);
    unsigned char* b = aligned_alloc(64, n);
    unsigned char* d = aligned_alloc(64, n);
    if (!a || !b || !d) return 1;
    memset(a, 1, n);
    memset(b, 2, n);

    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);
    vec_add_u8(d, a, b, n);
    clock_gettime(CLOCK_MONOTONIC, &t1);

    volatile unsigned s = 0;
    for (size_t i = 0; i < n; i += n/16) s += d[i];
    printf("sum=%u time=%.6f sec throughput=%.2f GB/s\n",
           s, sec(CLOCK_MONOTONIC, t0, t1), (double)n/sec(CLOCK_MONOTONIC, t0, t1)/1e9);
    free(a); free(b); free(d);
    return 0;
}

Собираем бенч и смотрим выгоду от v3/v4 без смены бинаря:

cc -O3 bench.c -lvecops -o bench

# 1) База
./bench 67108864

# 2) Отключаем AVX2, чтобы увидеть откат под baseline
GLIBC_TUNABLES="glibc.cpu.hwcaps=-AVX2_Usable" ./bench 67108864

# 3) Если у вас есть AVX-512 и собрано v4 — сравните
./bench 67108864
GLIBC_TUNABLES="glibc.cpu.hwcaps=-AVX512F_Usable,-AVX512BW_Usable" ./bench 67108864

На современных CPU обычно видим ощутимый рост пропускной способности между baseline и AVX2, а затем ещё прирост на AVX-512, если память и размеры задач подходящие. Точно сколько — зависит от железа и частоты переходов по кэшу. Важнее другое: один и тот же бинарь автоматически применяет правильный lib.

А если хочется оптимизировать сами бинарники

Есть инициатива расширить идею до исполняемых файлов: на входном PATH появится вендорный «хелпер», который будет искать v3/v4-варианты бинаря и запускать их, а иначе — baseline. Fedora это обсуждает и несёт в системный стек systemd. Это удобно, но пока не везде доступно штатно, поэтому для своих CLI я бы начинал именно с библиотек.

Итог

Чтобы включить x86-64-v3 для своего CLI, не нужен форк дистрибутива. Собираем критичные библиотеки с -march=x86-64-v3, кладём их в .../glibc-hwcaps/x86-64-v3/, проверяем SONAME и релятивные симлинки. Для OpenSSL пользуемся тем же приёмом и внимательно отслеживаем символ-версии. Для libm полагаемся на уже существующую IFUNC-диспетчеризацию в glibc и корректно используем GLIBC_TUNABLES для сравнения режимов. Для бенчей достаточно одного бинаря и нескольких библиотек, чтобы увидеть разницу на AVX2 и AVX-512.


Напоследок рекомендую обратить внимание на курс «Administrator Linux. Professional». Он про продвинутую админку без магии: сеть и storage, SELinux и systemd, безопасный деплой, мониторинг и автоматизацию (Zabbix/Prometheus, Ansible, Docker) с фокусом на Ubuntu 22.04. Чтобы узнать, подойдет ли вам программа курса, пройдите вступительный тест.

Технологии развиваются быстро. С подпиской OTUS берёте нужные курсы сейчас, а при смене приоритетов — корректируете трек без доплат. Выгоднее, чем оплачивать каждый курс отдельно. Узнать в деталях

Комментарии (0)