Отправьте статью сегодня! Журнал выйдет ..., печатный экземпляр отправим ...
Опубликовать статью

Молодой учёный

Архитектурная переносимость оптимизаций C-циклов

Информационные технологии
Препринт статьи
25.07.2026
Поделиться
Аннотация
В работе представлена экспериментальная оценка переносимости относительного эффекта локальных компиляторных оптимизаций — автовекторизации (-ftree-vectorize / -fvectorize), разворачивания циклов (-funroll-loops) и выноса инвариантных ветвлений (-funswitch-loops) — при переносе четырёх тестовых циклов на языке C с архитектуры x86–64 на AArch64. Дополнительно исследована роль флага -fassociative-math в разблокировке векторизации циклов с аккумулятором. Установлено, что знак оптимизационного эффекта сохраняется между архитектурами, однако его абсолютная величина детерминирована в основном шириной SIMD регистров (256 бит AVX2 против 128 бит NEON).
Библиографическое описание
Бабенков, Ю. М. Архитектурная переносимость оптимизаций C-циклов / Ю. М. Бабенков, И. А. Молчанов, Данила Лемза. — Текст : непосредственный // Молодой ученый. — 2026. — № 30 (633). — URL: https://moluch.ru/archive/633/139397.


С ростом доли процессоров на архитектуре 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]
vfmadd213ss xmm1,xmm0,DWORD PTR [rsi+rax*1]
vmovss DWORD PTR [rsi+rax*1],xmm1
add rax,0x4
cmp rdx,rax
jne
vmovss xmm0,DWORD PTR [rsi]

vmovups ymm1,YMMWORD PTR [rdi+rax*1]
vfmadd213ps ymm1,ymm2,YMMWORD PTR [rcx+rax*1]
vmovups YMMWORD PTR [rcx+rax*1],ymm1
add rax,0x20
cmp rax,rsi
jne
mov eax,edx

Таблица 5

Трансформация ассемблера SAXPY (GCC, AArch64)

Базовый уровень (скаляр)

vec_only (NEON, 4×float)

ldr s31, [x0, x3]
ldr s30, [x1, x3]
fmadd s30, s31, s0, s30
str s30, [x1, x3]
add x3, x3, #0x4
cmp x2, x3
b.ne

ldr q4, [x0, x3]
ldr q3, [x1, x3]
fmla v3.4s, v4.4s, v0.s[0]
str q3, [x1, x3]
add x3, x3, #0x10
cmp x3, x4
b.ne

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-параллелизм.

Влияние -fassociative-math на ускорение редукции

Рис. 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]
add rdi,0x4
cmp rdi,rax
jne
vmovss DWORD PTR [rsp-0x4],xmm0

vaddps ymm0,ymm0,YMMWORD PTR [rax]
add rax,0x20
cmp rdx,rax
jne
vextractf128 xmm1,ymm0,0x1

Таблица 7

Трансформация ассемблера Reduce (GCC, AArch64)

Базовый уровень (скаляр)

vec_assocmath (NEON)

ldr s30, [x0], #4
fadd s31, s31, s30
cmp x0, x1
b.ne

ldr q29, [x2], #16
fadd v0.4s, v0.4s, v29.4s
cmp x2, x3
b.ne

Clang генерирует структурно идентичный цикл.

3.3. Разворачивание циклов (-funroll-loops). Исходный код цикла Copy:

for (int i = 0; i < n; i++)
dst[i] = src[i];

Ускорение Copy: разворачивание и комбинация vec+unroll

Рис. 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]
vmovss DWORD PTR [rsi+rax*1],xmm0
add rax,0x4
cmp rax,rdx
jne
vmovss xmm0,DWORD PTR [rsi]

je
vmovss xmm7,DWORD PTR [rdi+rax*1]
vmovss DWORD PTR [rsi+rax*1],xmm7
vmovss xmm8,DWORD PTR [rdi+rax*1+0x4]
vmovss DWORD PTR [rsi+rax*1+0x4],xmm8
vmovss xmm9,DWORD PTR [rdi+rax*1+0x8]
vmovss DWORD PTR [rsi+rax*1+0x8],xmm9
vmovss xmm10,DWORD PTR [rdi+rax*1+0xc]
vmovss DWORD PTR [rsi+rax*1+0xc],xmm10
vmovss xmm11,DWORD PTR [rdi+rax*1+0x10]
vmovss DWORD PTR [rsi+rax*1+0x10],xmm11
vmovss xmm12,DWORD PTR [rdi+rax*1+0x14]
vmovss DWORD PTR [rsi+rax*1+0x14],xmm12
vmovss xmm13,DWORD PTR [rdi+rax*1+0x18]

vmovups ymm0,YMMWORD PTR [rdi+rax*1]
vmovups YMMWORD PTR [rcx+rax*1],ymm0
add rax,0x20
cmp rax,rsi
jne
mov eax,edx

Таблица 9

Трансформация ассемблера Copy (GCC, AArch64)

Базовый уровень

unroll_only

vec_only (NEON)

ldr s31, [x0, x3]
str s31, [x1, x3]
add x3, x3, #0x4
cmp x3, x2
b.ne

ldr s6, [x0, x3]
str s6, [x1, x3]
ldr s7, [x0, x8]
str s7, [x1, x8]
ldr s16, [x0, x9]
str s16, [x1, x9]
ldr s17, [x0, x10]
str s17, [x1, x10]
...(8× ldr/str, шаг +0x20)
cmp x3, x2
b.ne

ldr q31, [x0, x3]
str q31, [x1, x3]
add x3, x3, #0x10
cmp x3, x4
b.ne

Базовый уровень: скалярная загрузка 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 клонирует цикл, проверяя условие однократно перед циклом, открывая каждую копию для автовекторизации.

Ускорение Unswitch: vec_only vs полная комбинация

Рис. 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]
vaddss xmm0,xmm0,xmm1
vmovss DWORD PTR [rsi+rax*1],xmm0
add rax,0x4
cmp rax,rdx
jne
vmovss xmm0,DWORD PTR [rsi]

je
vaddps ymm12,ymm4,YMMWORD PTR [rdi+rdx*1]
vmovups YMMWORD PTR [rax+rdx*1],ymm12
vaddps ymm13,ymm4,YMMWORD PTR [rdi+rdx*1+0x20]
vmovups YMMWORD PTR [rax+rdx*1+0x20],ymm13
vaddps ymm14,ymm4,YMMWORD PTR [rdi+rdx*1+0x40]
vmovups YMMWORD PTR [rax+rdx*1+0x40],ymm14
vaddps ymm15,ymm4,YMMWORD PTR [rdi+rdx*1+0x60]
vmovups YMMWORD PTR [rax+rdx*1+0x60],ymm15
vaddps ymm0,ymm4,YMMWORD PTR [rdi+rdx*1+0x80]
vmovups YMMWORD PTR [rax+rdx*1+0x80],ymm0
vaddps ymm1,ymm4,YMMWORD PTR [rdi+rdx*1+0xa0]
vmovups YMMWORD PTR [rax+rdx*1+0xa0],ymm1
vaddps ymm2,ymm4,YMMWORD PTR [rdi+rdx*1+0xc0]

Таблица 11

Трансформация ассемблера Unswitch (GCC, AArch64)

Базовый уровень (ветвл.)

vec+unroll+unswitch (NEON)

cbnz w3, .true_branch
fadd s0, s31, s30
add x3, x4, #0x4
str s0, [x1, x4]
cmp x3, x2
b.eq .exit
ldr s29, [x0, x3]
add x4, x4, #0x8
fadd s29, s29, s30
str s29, [x1, x3]
cmp x2, x4
b.eq .exit
ldr s31, [x0, x4]
b .loop_top

ldr q21, [x0, x15]
fadd v23.4s, v21.4s, v22.4s
str q23, [x1, x15]
add x15, x15, #0x10
ldr q24, [x0, x15]
fadd v31.4s, v24.4s, v22.4s
str q31, [x1, x15]
add x15, x15, #0x10
ldr q0, [x0, x15]
fadd v30.4s, v0.4s, v22.4s
str q30, [x1, x15]
add x15, x15, #0x10

x86–64: условный test/je внутри цикла устранён; вместо него — развёрнутый SIMD-цикл с vaddps ymm (8 float). AArch64: cbnz w3 вынесен перед циклом; NEON-цикл с fadd v23.4s (4 float). Clang генерирует аналогичный SIMD-код уже при vec_only.

Заключение

Проведённый эксперимент позволяет сформулировать следующие выводы:

  1. Знак эффекта портируется. Если флаг ускоряет цикл на x86–64, он ускоряет его и на AArch64. Ни одного случая инверсии знака не обнаружено.
  2. Масштаб эффекта не портируется. Ускорение от автовекторизации на x86–64 в 2–3 раза выше, чем на AArch64, что коррелирует с двукратной разницей в ширине SIMD-регистров (256 vs 128 бит).
  3. -fassociative-math достаточен для разблокировки векторизации редукции; -ffast-math избыточен (побайтово идентичный ассемблер) [9].
  4. Дополнительно проверен цикл с мультипликативной loop-carried зависимостью (acc = acc · 0.99 + a [i]): все исследованные флаги показали +0 % на обеих архитектурах, что подтверждает кроссплатформенную иммунность данного паттерна.
  5. Вынос ветвлений — наименее переносимая по масштабу оптимизация.

Ограничения и угрозы валидности

Результаты получены на GCC 14.2/15.1 и Clang 19.1. Поведение проходов может измениться в будущих версиях. Четыре паттерна охватывают основные типы числовых циклов. Циклы с нерегулярным доступом к памяти (scatter/gather), зависимостями по индексу или полиморфными вызовами не рассматривались. Стенд x86–64 использует AVX2 (256 бит). На процессорах с AVX-512 абсолютные значения ускорений будут иными [4]. Аналогично для AArch64 с SVE/SVE2. Приводимые числовые значения характеризуют конкретные стенды. Направление эффектов обосновано структурой генерируемого ассемблера и воспроизведено на дополнительных процессорах.

Литература:

  1. Callahan D., Dongarra J., Levine D. Vectorizing Compilers: A Test Suite and Results // Proc. ACM/IEEE Supercomputing. — 1988. — P. 98–105.
  2. Maleki S., Gao Y., Garzarán M. J. et al. An Evaluation of Vectorizing Compilers // Proc. PACT. — IEEE, 2011. — P. 372–382.
  3. Popov I., Chesnokov A. Comparison of Vectorization Capabilities of Different Compilers for X86 and ARM CPUs // arXiv:2502.11906. — 2025.
  4. Шабанов Б. М., Телегин П. Н., Рыбаков А. А. Проблемы векторизации гнёзд циклов с использованием инструкций AVX-512 // Программная инженерия. — 2018. — Т. 9, № 5. — С. 203–213.
  5. Fog A. Optimizing Software in C++. — Copenhagen, 2024. — URL: https://www.agner.org/optimize/optimizing_cpp.pdf.
  6. Mytkowicz T. et al. Producing Wrong Data Without Doing Anything Obviously Wrong! // Proc. ASPLOS. — ACM, 2009. — P. 265–276.
  7. Drepper U. What Every Programmer Should Know About Memory. — Red Hat, 2007. — 114 p.
  8. Optimize Options // Документация GCC 14.2. — URL: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html.
  9. Byrne S. Beware of fast-math. — 2021. — URL: https://simonbyrne.github.io/notes/fastmath/.
  10. Branch predictor: How many "if"s are too many? // Cloudflare Blog. — 2025. — URL: https://blog.cloudflare.com/branch-predictor/.
Можно быстро и просто опубликовать свою научную статью в журнале «Молодой Ученый». Сразу предоставляем препринт и справку о публикации.
Опубликовать статью
Молодой учёный №30 (633) июль 2026 г.
📄 Препринт
Файл будет доступен после публикации номера
Похожие статьи
Современные методы оптимизации программного кода
Анализ способов оптимизации программного кода с использованием возможностей современных многоядерных процессоров и графических карт
Оптимизация работы программы по скорости методами программирования без условных операторов
Оценка скорости вычисления тригонометрических функций на Си
Сравнение эффективности использования технологий CUDA и OpenCL при реализации нейронной сети репликации
Влияние квантизации больших языковых моделей на качество кодогенерации для компилируемых и интерпретируемых языков
Ускорение расчета динамического напряженно-деформированного состояния на графических ускорителях с использованием OpenCL
Исследование изменения скорости выполнения программ из-за промахов кэша процессора
Сравнительный анализ исполнения алгоритмов, содержащих ветвления в архитектуре IA-32 и IA-64.
Практический анализ уязвимостей спекулятивного выполнения: тайминги кэш-памяти и эффективность защиты Linux

Молодой учёный