С ростом доли процессоров на архитектуре AArch64 в серверном (AWS Graviton, Ampere Altra), пользовательском (Apple Silicon) и встраиваемом сегментах перед разработчиками вычислительного ПО на языке C встаёт задача кроссплатформенного портирования критичных по производительности участков кода.
Компиляторы GCC и Clang предоставляют не только агрегированные уровни оптимизации (-O1, -O2, -O3), но и отдельные флаги, управляющие конкретными проходами (passes) оптимизирующего конвейера. При этом состав активных проходов различается: GCC активирует автовекторизацию (-ftree-vectorize) только начиная с -O3, тогда как Clang включает соответствующий проход (-fvectorize) уже на уровне -O2.
Цель настоящей работы — экспериментально установить, сохраняет ли изолированный компиляторный флаг свой относительный эффект при переносе с x86–64 на AArch64.
Таблица 1
Исследуемые флаги оптимизации и их механизм действия
|
Флаг |
Механизм действия |
|
-ftree-vectorize / -fvectorize |
Замена скалярных операций на SIMD-инструкции (AVX2: 8×float, NEON: 4×float) |
|
-funroll-loops |
Дублирование тела цикла для снижения накладных расходов на управление (cmp/branch) |
|
-funswitch-loops |
Вынос loop-invariant условия за пределы цикла с клонированием тела |
|
-fassociative-math |
Разрешение перестановки FP-операций (нарушение IEEE 754) для разблокировки параллельной редукции |
Отобраны четыре тестовых цикла, каждый из которых изолирует конкретный паттерн зависимости данных (табл. 2). Все массивы ограничены объёмом 4096 элементов × 4 байта = 16 КБ, что гарантирует полное размещение в кэше L1d (32–48 КБ) и превращает замер в compute-bound задачу [7].
Таблица 2
Тестовые циклы и целевая оптимизация
|
Цикл |
Паттерн |
Целевой флаг |
|
SAXPY |
y [i] = a·x [i] + y [i] |
-ftree-vectorize |
|
Reduce |
acc += a [i] |
-fassociative-math |
|
Copy |
dst [i] = src [i] |
-funroll-loops, -ftree-vectorize |
|
Unswitch |
if(mode) b [i]=a [i]*2 else b [i]=a [i]+1 |
-funswitch-loops |
Хронометраж осуществляется вызовом clock_gettime(CLOCK_MONOTONIC) с наносекундной точностью. Каждый исполняемый файл запускался 200 раз. Основной метрикой является медиана, для каждого замера вычисляется 95 % доверительный интервал [6]: CI₉₅ = 1.96 × σ / √n. На диаграммах доверительный интервал обозначен горизонтальными усами.
Также для снижения уровня шума была строго зафиксирована тактовая частота ЦП и остановлены фоновые процессы.
Таблица 3
Параметры тестовых стендов
|
Параметр |
Стенд x86–64 |
Стенд AArch64 |
|
Процессор |
Intel Core i5–8265U |
Apple M1 |
|
Базовая частота |
1.6 ГГц (зафикс.) |
2.064 ГГц |
|
ISA / SIMD |
x86–64, AVX2 (256 бит) |
AArch64, NEON (128 бит) |
|
Кэш L1d |
32 КБ, 8-way |
128 КБ, 8-way |
|
ОС |
Debian 12, ядро 6.1 |
Fedora 43, ядро 6.18 (Asahi Linux) |
|
GCC |
14.2 |
15.1 |
|
Clang |
19.1 (LLVM) |
19.1 (LLVM) |
|
Базовые флаги |
-O2 -march=native |
-O2 -march=native |
Для каждого цикла собрано 8 профилей флагов в двух компиляторах на двух архитектурах. Два компилятора использованы не для их сравнения, а для проверки устойчивости наблюдаемых закономерностей: если эффект флага воспроизводится и в GCC, и в Clang, он с большей вероятностью обусловлен природой трансформации, а не деталями реализации конкретного компилятора.
Автовекторизация (-ftree-vectorize / -fvectorize). Код тестового цикла:
for (int i = 0; i < n; i++)
y[i] += a * x[i];
Цикл SAXPY — идеальный паттерн для автовекторизации: отсутствие зависимостей между итерациями, линейный доступ к памяти, единственная FMA-операция в теле.
Рис. 1. Ускорение от автовекторизации
При -O2 GCC не выполняет автовекторизацию (+0 %). Явное включение -ftree-vectorize даёт +683 % (x86–64) и +283 % (AArch64). Clang при -O2 активирует -fvectorize автоматически, поэтому уже при базовых флагах демонстрирует +952 % (x86–64) и +265 % (AArch64).
Таблица 4
Трансформация ассемблера SAXPY (GCC, x86–64)
|
Базовый уровень (скаляр) |
vec_only (AVX2, 8×float) |
|
vmovss xmm1,DWORD PTR [rdi+rax*1]
|
vmovups ymm1,YMMWORD PTR [rdi+rax*1]
|
Таблица 5
Трансформация ассемблера SAXPY (GCC, AArch64)
|
Базовый уровень (скаляр) |
vec_only (NEON, 4×float) |
|
ldr s31, [x0, x3]
|
ldr q4, [x0, x3]
|
x86–64: скалярная vfmadd213ss (xmm, 1 float за такт) заменяется на vfmadd231ps (ymm, 8 float, шаг +0x20). AArch64: fmadd s30 заменяется на fmla v3.4s (Q-регистр, 4 float, шаг +0x10). Clang генерирует аналогичное преобразование на обеих платформах.
Ослабление ассоциативности (-fassociative-math). Код цикла Reduce:
float acc = 0.0f;
for (int i = 0; i < n; i++)
acc += a[i];
Зависимость по аккумулятору блокирует векторизацию: стандарт IEEE 754 запрещает переупорядочивание FP-сложений [9]. Флаг -ftree-vectorize сам по себе не даёт эффекта (+0 %). Только -fassociative-math разрешает компилятору разбить аккумулятор на независимые частичные суммы, что разблокирует SIMD-параллелизм.
Рис. 2. Влияние -fassociative-math на ускорение редукции
Ускорение: +752 % (x86–64, GCC) и +323 % (AArch64, GCC) — близко к теоретическому пределу, определяемому шириной SIMD-регистра (8 и 4 float соответственно).
Таблица 6
Трансформация ассемблера Reduce (GCC, x86–64)
|
Базовый уровень (скаляр) |
vec_assocmath (AVX2) |
|
vaddss xmm0,xmm0,DWORD PTR [rdi]
|
vaddps ymm0,ymm0,YMMWORD PTR [rax]
|
Таблица 7
Трансформация ассемблера Reduce (GCC, AArch64)
|
Базовый уровень (скаляр) |
vec_assocmath (NEON) |
|
ldr s30, [x0], #4
|
ldr q29, [x2], #16
|
Clang генерирует структурно идентичный цикл.
3.3. Разворачивание циклов (-funroll-loops). Исходный код цикла Copy:
for (int i = 0; i < n; i++)
dst[i] = src[i];
Рис. 3. Ускорение Copy: разворачивание и комбинация vec+unroll
Разворачивание без векторизации: GCC +101 % (x86–64), +90 % (AArch64). Комбинация vec+unroll: +1309 % (GCC x86–64), +501 % (GCC AArch64). Clang -funroll-loops на AArch64 не оказывает эффекта (+0 %), хотя на x86–64 ускорение составляет +100 %.
Таблица 8
Трансформация ассемблера Copy (GCC, x86–64)
|
Базовый уровень |
unroll_only |
vec_only (AVX2) |
|
vmovss xmm0,DWORD PTR [rdi+rax*1]
|
je
|
vmovups ymm0,YMMWORD PTR [rdi+rax*1]
|
Таблица 9
Трансформация ассемблера Copy (GCC, AArch64)
|
Базовый уровень |
unroll_only |
vec_only (NEON) |
|
ldr s31, [x0, x3]
|
ldr s6, [x0, x3]
|
ldr q31, [x0, x3]
|
Базовый уровень: скалярная загрузка vmovss / ldr s31 (4 байта). unroll_only: компилятор дублирует тело цикла, снижая накладные расходы на cmp/branch. vec_only: замена на vmovups ymm (32 байта, x86–64) / ldr q31 (16 байт, AArch64). Clang генерирует аналогичное преобразование.
Исходный код цикла Unswitch:
for (int i = 0; i < n; i++) {
if (mode)
b[i] = a[i] * 2.0f;
else
b[i] = a[i] + 1.0f;
}
Условие if(mode) инвариантно относительно итерационной переменной. Флаг -funswitch-loops клонирует цикл, проверяя условие однократно перед циклом, открывая каждую копию для автовекторизации.
Рис. 4. Ускорение Unswitch: vec_only vs полная комбинация
GCC vec_only без unswitching: +0 % — ветвление внутри цикла блокирует векторизацию. Полная комбинация: +1224 % (x86–64), +390 % (AArch64). Clang выполняет unswitching автоматически при -O2, поэтому Clang vec_only: +1080 % (x86–64), +281 % (AArch64).
GCC unroll_only на x86–64: -38 % — разворачивание цикла с ветвлением без предварительного выноса увеличивает давление на I-cache и размножает точки предсказания [10].
Таблица 10
Трансформация ассемблера Unswitch (GCC, x86–64)
|
Базовый уровень (ветвл.) |
vec+unroll+unswitch (AVX2) |
|
vmovss xmm0,DWORD PTR [rdi+rax*1]
|
je
|
Таблица 11
Трансформация ассемблера Unswitch (GCC, AArch64)
|
Базовый уровень (ветвл.) |
vec+unroll+unswitch (NEON) |
|
cbnz w3, .true_branch
|
ldr q21, [x0, x15]
|
x86–64: условный test/je внутри цикла устранён; вместо него — развёрнутый SIMD-цикл с vaddps ymm (8 float). AArch64: cbnz w3 вынесен перед циклом; NEON-цикл с fadd v23.4s (4 float). Clang генерирует аналогичный SIMD-код уже при vec_only.
Заключение
Проведённый эксперимент позволяет сформулировать следующие выводы:
- Знак эффекта портируется. Если флаг ускоряет цикл на x86–64, он ускоряет его и на AArch64. Ни одного случая инверсии знака не обнаружено.
- Масштаб эффекта не портируется. Ускорение от автовекторизации на x86–64 в 2–3 раза выше, чем на AArch64, что коррелирует с двукратной разницей в ширине SIMD-регистров (256 vs 128 бит).
- -fassociative-math достаточен для разблокировки векторизации редукции; -ffast-math избыточен (побайтово идентичный ассемблер) [9].
- Дополнительно проверен цикл с мультипликативной loop-carried зависимостью (acc = acc · 0.99 + a [i]): все исследованные флаги показали +0 % на обеих архитектурах, что подтверждает кроссплатформенную иммунность данного паттерна.
- Вынос ветвлений — наименее переносимая по масштабу оптимизация.
Ограничения и угрозы валидности
Результаты получены на GCC 14.2/15.1 и Clang 19.1. Поведение проходов может измениться в будущих версиях. Четыре паттерна охватывают основные типы числовых циклов. Циклы с нерегулярным доступом к памяти (scatter/gather), зависимостями по индексу или полиморфными вызовами не рассматривались. Стенд x86–64 использует AVX2 (256 бит). На процессорах с AVX-512 абсолютные значения ускорений будут иными [4]. Аналогично для AArch64 с SVE/SVE2. Приводимые числовые значения характеризуют конкретные стенды. Направление эффектов обосновано структурой генерируемого ассемблера и воспроизведено на дополнительных процессорах.
Литература:
- Callahan D., Dongarra J., Levine D. Vectorizing Compilers: A Test Suite and Results // Proc. ACM/IEEE Supercomputing. — 1988. — P. 98–105.
- Maleki S., Gao Y., Garzarán M. J. et al. An Evaluation of Vectorizing Compilers // Proc. PACT. — IEEE, 2011. — P. 372–382.
- Popov I., Chesnokov A. Comparison of Vectorization Capabilities of Different Compilers for X86 and ARM CPUs // arXiv:2502.11906. — 2025.
- Шабанов Б. М., Телегин П. Н., Рыбаков А. А. Проблемы векторизации гнёзд циклов с использованием инструкций AVX-512 // Программная инженерия. — 2018. — Т. 9, № 5. — С. 203–213.
- Fog A. Optimizing Software in C++. — Copenhagen, 2024. — URL: https://www.agner.org/optimize/optimizing_cpp.pdf.
- Mytkowicz T. et al. Producing Wrong Data Without Doing Anything Obviously Wrong! // Proc. ASPLOS. — ACM, 2009. — P. 265–276.
- Drepper U. What Every Programmer Should Know About Memory. — Red Hat, 2007. — 114 p.
- Optimize Options // Документация GCC 14.2. — URL: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html.
- Byrne S. Beware of fast-math. — 2021. — URL: https://simonbyrne.github.io/notes/fastmath/.
- Branch predictor: How many "if"s are too many? // Cloudflare Blog. — 2025. — URL: https://blog.cloudflare.com/branch-predictor/.

