Разработчик Lucas Lima Freitag из UFRN протестировал 28 конфигураций умножения матриц (GEMM) одинарной точности на одном ядре AMD Zen 3 — AMD Ryzen 5 5500. Он использовал C++ intrinsics с наборами инструкций AVX2 и FMA. Лучший результат показала модель MX24: 85.30 GFLOPS, что составляет 63.5% от теоретического пика в 134.4 GFLOPS. Это в 56 с лишним раз быстрее наивной реализации и сравнимо с оптимизированными библиотеками вроде AMD AOCL и OpenBLAS.
Freitag перебрал комбинации разных техник. Cache blocking (tiling) дробит матрицы под кэши L1 (32 KB), L2 (512 KB) и L3 (16 MB). Оптимальный размер блока BK по K оказался 256 — максимизирует переиспользование данных, не выходя за L2. Register blocking держит аккумуляторы матрицы C в регистрах YMM. Четыре строки (4-line) дали лучший баланс: хотя это требует spill (16 дополнительных регистров), переиспользование матрицы A окупает затраты.
Ключевой приём — FMA chaining. На Zen 3 инструкция FMA выполняется 4 цикла, поэтому нужно несколько независимых цепочек вычислений, чтобы загрузить обе порты FMA на полную. Модель с четырьмя аккумуляторами (chain4) выжала максимум.
Упаковка матрицы B (B-pack on-the-fly) копирует её блоки в непрерывный буфер, заменяя нерегулярные чтения на последовательные. Это оказалось эффективнее, чем полная транспозиция (которая засоряет L3) и прямой доступ (TLB-промахи). Выравнивание памяти на 32 байта под vmovaps дало прирост около 5%.
А вот prefetching вручную через _mm_prefetch снизил производительность на 8% — аппаратный префетчер Zen 3 и так справляется с потоковыми паттернами. Non-temporal stores (_mm256_stream_ps) вообще стали катастрофой: 1.24 GFLOPS. Потому что матрица C читается, меняется и пишется обратно, а stream-инструкция каждый раз инвалидирует кеш-линию, заставляя грузить данные из DRAM заново.
Другие провальные эксперименты: 8 строк в регистрах (YMM не хватает, 48 переменных уходят в spill) — 12.24 GFLOPS; цепочка из 6 аккумуляторов (chain6) — 14.26 GFLOPS; попытка держать всю матрицу C в регистрах через внешний k-block — 9.13 GFLOPS. Крупный блок BI=128 при BK=256 дал working set 128 KB, что вылезло за L1 и вызвало множество промахов — всего 8.38 GFLOPS.
Три лучшие модели (MX24, MX22, MX23) бит-идентичны эталону — ошибка ноль. Остальные дают погрешность порядка 1e-6, что нормально для float. Весь код на C++ с intrinsics, собирается под GCC 12.2+ с флагами -O3 -mavx2 -mfma -march=native. Репозиторий включает Makefile, полные логи и научную статью в формате LaTeX.