Привет, Хабр!
Обычно я за то, чтобы не усложнять рантайм лишний раз, но здесь случай приятный: можно включить у своих 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 берёте нужные курсы сейчас, а при смене приоритетов — корректируете трек без доплат. Выгоднее, чем оплачивать каждый курс отдельно. Узнать в деталях