15. AVX, AVX2, AVX-512 и ограничения SIMD

В предыдущей главе 14. SIMD. Single Instruction Multiple Data мы познакомились с ошеломляющей мощью векторной обработки: способностью за один такт процессора перемалывать не одиночные числа, а целые массивы данных.

На первый взгляд, перед архитектором открывается очевидный и заманчивый путь. Если 128-битные регистры ускоряют вычисления в 4 раза, а 256-битные — в 8 раз, почему бы не продолжить масштабирование? Почему не сделать векторные регистры шириной 1024, 2048 или 8192 бита, чтобы складывать мегабайты информации за один такт?

В этот момент программист лицом к лицу сталкивается с неумолимыми законами физики кремния, электродинамики и термодинамики. Векторные вычисления — это не бесплатный подарок. У них есть своя суровая, а порой и скрыто-разрушительная цена.

В этой лекции мы проследим эволюцию векторных расширений x86-64, разберем феномен AVX Offset (аппаратного троттлинга частоты), столкнемся со Стеной памяти и научимся безопасно работать с зоопарком серверных процессоров в коде на Go.


Эволюция SIMD в архитектуре x86-64

Векторные инструкции не появились в процессорах одномоментно. Они эволюционировали десятилетиями, отражая способность инженеров упаковывать всё больше транзисторов на кремниевую пластину:

flowchart LR
    subgraph Timeline ["Хронология развития SIMD в архитектуре x86-64"]
        direction TB
        SSE["<b>SSE (1999–2006)</b><br>128 бит (16 байт)<br>Регистры XMM (X0–X15)<br>100% совместимость на всех CPU"]
        AVX["<b>AVX / AVX2 (2011–2013)</b><br>256 бит (32 байта)<br>Регистры YMM (Y0–Y15)<br>FMA (Fused Multiply-Add)<br>Золотой стандарт серверного бэкенда"]
        AVX512["<b>AVX-512 (2016+)</b><br>512 бит (64 байта)<br>Регистры ZMM (Z0–Z31)<br>Размер Cache Line, маски k0–k7<br>Высокое тепловыделение и троттлинг"]
        AVX10["<b>AVX10 (2024+)</b><br>Унификация AVX-512 для P- и E-ядер<br>(ревизия 2025: 512 бит обязательны)"]
    end

    SSE --> AVX
    AVX --> AVX512
    AVX512 --> AVX10

    classDef sse fill:#131d33,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
    classDef avx fill:#1e293b,stroke:#10b981,stroke-width:1px,color:#f8fafc;
    classDef avx512 fill:#0f172a,stroke:#f43f5e,stroke-width:1px,color:#f8fafc;
    classDef avx10 fill:#1e293b,stroke:#6366f1,stroke-width:1px,color:#f8fafc;
    class SSE sse;
    class AVX avx;
    class AVX512 avx512;
    class AVX10 avx10;
  
  1. SSE (Streaming SIMD Extensions, SSE2, SSE3, SSE4.2):
    Базовый стандарт мира x86-64. Ширина регистров — 128 бит (16 байт). Регистры называются XMM (от X0 до X15). Это «прожиточный минимум»: SSE2 гарантированно присутствует на всех 64-битных процессорах x86 (а значит, и в базовом уровне GOAMD64=v1 в Go). SSE3, SSSE3, SSE4.1/4.2 и POPCNT в v1 не гарантированы: на практике они есть на любом современном железе, но Go опирается на них только с GOAMD64=v2, а AVX/AVX2 — с v3.
  2. AVX и AVX2 (Advanced Vector Extensions):
    Регистры расширили ровно вдвое — до 256 бит (32 байта), назвав их YMM.
    AVX принес неразрушающий синтаксис из трех операндов (VADDPS dst, src1, src2), а AVX2 добавил целочисленные 256-битные операции. Рядом с ними появилось отдельное расширение FMA3 (Fused Multiply-Add) — операция $A \times B + C$ с одинарным округлением; это самостоятельный бит в CPUID, не обязательно присутствующий там, где есть AVX2. Выполняется она не «за один такт»: латентность FMA — около 4–5 тактов, но блоки FMA конвейеризованы и принимают новую операцию каждый такт.
    На сегодняшний день AVX2 — это золотой стандарт (Sweet Spot) для серверного бэкенда: он удваивает производительность без критических побочных эффектов.
  3. AVX-512:
    Ширина регистров достигла гигантских 512 бит (64 байта). Регистры называются ZMM (32 регистра от Z0 до Z31). В один регистр за один такт укладывается ровно одна кэш-линия памяти! Но именно здесь разработчики процессоров уперлись в жестокий физический потолок.

Mechanical Sympathy: Нагрев и AVX Offset

Чтобы понять, почему индустрия не спешит делать 1024-битные регистры, нужно вспомнить физику из 2. Транзисторы, биты и логические вентили.

Динамическая мощность, потребляемая CMOS-транзисторами при переключении, описывается базовой формулой:
$$P = C \times V^2 \times f$$
где $C$ — емкость затворов, $V$ — напряжение питания, а $f$ — тактовая частота.

Обычная скалярная операция ADD RAX, RBX включает скромную 64-битную цепь сумматора.
Векторная инструкция AVX-512 активирует гигантский каскад 512-битных сумматоров — это в 8 раз больше одновременно переключающихся транзисторов!

Когда процессор начинает на каждом такте молотить 512-битные числа, по кремниевым дорожкам течет колоссальный импульсный ток. Возникает резкий скачок температуры (Thermal Spike) и локальная просадка напряжения питания ($L \cdot \frac{di}{dt}$ voltage droop). Если ничего не предпринимать, кристалл очень быстро выйдет за пределы теплового пакета (TDP) и будет вынужден сам срезать частоту, чтобы не перегреться.

Чтобы не выходить за пределы по току и тепловыделению, инженеры Intel внедрили механизм AVX Offset (Frequency Downclocking): частота ядра выбирается по «лицензии» (license level), зависящей от того, насколько тяжёлые векторные инструкции исполняются. (Цифры ниже — иллюстрация эпохи Skylake-SP; конкретные значения зависят от модели CPU.)

Как только ядро начинает исполнять поток «тяжелых» инструкций AVX-512, процессор аппаратно сбрасывает тактовую частоту всего ядра (на процессорах того поколения):

flowchart TD
    State1["Скалярный код (SISD) или SSE<br>Частота: <b>4.8 GHz</b><br>Напряжение: норма"] -->|"Появление инструкций AVX2"| State2["Код AVX2 (256 бит)<br>Легкий троттлинг: <b>4.3 GHz</b>"]
    State2 -->|"Появление тяжелых инструкций ZMM"| State3["<b>AVX-512 (512 бит)</b><br>Агрессивный троттлинг: <b>3.2 GHz</b><br>Падение частоты на 30%!"]
    State3 -->|"Векторный блок завершил работу"| CoolDown["Гистерезис (остывание ядра ~1–2 мс)"]
    CoolDown --> State1

    classDef normal fill:#1e293b,stroke:#10b981,stroke-width:1px,color:#f8fafc;
    classDef warn fill:#1e293b,stroke:#f59e0b,stroke-width:1px,color:#f8fafc;
    classDef danger fill:#0f172a,stroke:#f43f5e,stroke-width:1px,color:#f8fafc;
    class State1 normal;
    class State2,CoolDown warn;
    class State3 danger;
  


Стена Памяти (Memory Wall)

Второе фундаментальное препятствие для расширения векторов — пропускная способность подсистемы памяти.

В статье про конвейеризацию мы определили ядро процессора как «голодного зверя». Сверхширокий SIMD превращает этот голод в черную дыру.

Давайте посчитаем:
Если ядро способно за один такт переваривать один 64-байтный регистр ZMM (AVX-512), работая на частоте $4.0\text{ ГГц}$, ему требуется поток входных данных со скоростью:
$$\text{Пропускная способность} = 64 \text{ байта} \times 4.0 \times 10^9 \text{ тактов/с} = 256 \text{ ГБ/с на ОДНО ядро!}$$

А теперь взглянем на физическую реальность серверной материнской платы:
Для сравнения: двухканальная оперативная память DDR5-5600 (типичный десктоп) выдает около 80–90 ГБ/с, а у серверных процессоров 8–12 каналов памяти дают несколько сотен ГБ/с на сокет — но и их делят между собой десятки ядер!

Вывод жёсткий: если данные лежат в оперативной памяти (DRAM), широкие векторные инструкции почти не ускоряют код, упирающийся в пропускную способность памяти.
Процессор сожрет первые 64 байта за 1 такт, а затем будет ждать следующую порцию из RAM. (Аппаратные префетчеры и параллелизм на уровне памяти — MLP — смягчают эту картину при последовательном доступе, но предел пропускной способности остаётся неизменным.) Вся мощь векторных сумматоров разобьется о стену памяти.

Векторизация дает реальный выигрыш только тогда, когда алгоритм обладает высокой арифметической плотностью (Arithmetic Intensity) — то есть данные многократно переиспользуются, находясь в молниеносных кэшах L1d и L2 (подробнее мы изучим это в 17. Пирамида памяти. Регистры, SRAM, DRAM и цена доступа).


Фрагментация ISA: Пишем код под зоопарк процессоров

В отличие от базового набора инструкций x86-64, наличие поддержки AVX2 или AVX-512 не гарантировано:

  • Бюджетные серверы, виртуальные машины VPS или процессоры Intel Celeron/Atom поддерживают только базовый SSE;
  • Процессоры Intel Alder Lake / Raptor Lake отключали AVX-512 из-за гетерогенной архитектуры с энергоэффективными E-ядрами;
  • Серверные процессоры AMD EPYC и Intel Xeon поддерживают разные подмножества инструкций (AVX-512F, AVX-512BW, AVX-512CD...).

Если скомпилировать бинарник на ассемблере с инструкцией VPADDQ ZMM0..., а затем запустить его на старом сервере без поддержки AVX-512, ядро Linux не сможет ее декодировать. Операционная система пошлет процессу сигнал SIGILL (Illegal Instruction), и ваш Go-сервис моментально упадет с паникой и аварийным дампом памяти (Core Dump).

Решение в Go: CPUID и Динамическая диспетчеризация

Чтобы безопасно использовать мощь SIMD, разработчики библиотек на Go применяют Динамическую диспетчеризацию (Dynamic Dispatch).

При старте программы опрашивается инструкция CPUID, выясняются физические возможности кристалла, и глобальный указатель функции переключается на наиболее оптимальную реализацию.

В стандартном репозитории Go для этого есть официальный пакет golang.org/x/sys/cpu:

Go
package main

import (
    "fmt"
    "golang.org/x/sys/cpu"
)

// hashFunc — глобальный функциональный указатель
var hashFunc func(data []byte) []byte

func init() {
    // Опрос физических возможностей кристалла через CPUID в рантайме
    if cpu.X86.HasAVX512 {
        fmt.Println("[Init] Обнаружен AVX-512: активируем экстремальный векторный путь")
        hashFunc = hashAVX512
    } else if cpu.X86.HasAVX2 {
        fmt.Println("[Init] Обнаружен AVX2: активируем сбалансированный векторный путь")
        hashFunc = hashAVX2
    } else {
        fmt.Println("[Init] AVX не поддерживается: фолбэк на скалярный код (SISD / базовый SSE)")
        hashFunc = hashGeneric
    }
}

// Заглушки функций (в реальности реализованы на ассемблере Plan 9)
func hashAVX512(data []byte) []byte {
    // Использует регистры ZMM (512 бит)
    return data
}

func hashAVX2(data []byte) []byte {
    // Использует регистры YMM (256 бит)
    return data
}

func hashGeneric(data []byte) []byte {
    // Чистый Go, обычный цикл 'for', работающий на любых машинах
    return data
}

func main() {
    payload := []byte("high-performance-data-payload")
    result := hashFunc(payload)
    _ = result
}


Итог

  1. Эволюция векторов x86-64: SSE (128 бит / XMM), AVX2 (256 бит / YMM), AVX-512 (512 бит / ZMM).
  2. AVX Offset (Downclocking): переключение сотен 512-битных сумматоров вызывает бросок тока и перегрев. Чтобы спасти кристалл, процессор может снижать тактовую частоту ядра (на процессорах эпохи Skylake-SP — на десятки процентов; на новых — заметно меньше), что может непреднамеренно замедлить скалярный код соседних горутин.
  3. Стена Памяти (Memory Wall): ядро с AVX-512 требует до 256 ГБ/с пропускной способности. Если данные не помещаются в L1d/L2 кэш, широкие векторные блоки простаивают в ожидании оперативной памяти DRAM.
  4. Фрагментация ISA: использование неподдерживаемых инструкций вызывает аварийный сигнал SIGILL. В Go-проектах применяется пакет golang.org/x/sys/cpu для динамического выбора реализации алгоритма во время старта.
  5. Накладные расходы: ассемблерные функции не инлайнятся компилятором Go. Применение SIMD к крошечным объемам данных (до 32 байт) делает код медленнее из-за накладных расходов на CALL и RET.

Теперь нам ясны все технические нюансы и ограничения SIMD. Но здесь встает главный вопрос любого Go-разработчика:
Если компиляторы GCC, Clang и Rustc тратят столько усилий на автоматическую векторизацию, почему создатели Go упорно отказываются внедрять автовекторизатор в стандартный cmd/compile? Зачем они заставляют писать на ассемблере Plan 9 вручную?

В следующей статье мы раскроем тайну философии и компилятора Go: 16. Почему Go почти не использует SIMD автоматически.