14. SIMD. Single Instruction Multiple Data

В предыдущих лекциях, посвященных конвейеризации, внеочередному исполнению и 13. Предсказание ветвлений и Спекулятивное исполнение, мы наблюдали, как кремниевые инженеры выжимают последние капли производительности из классического последовательного кода. Этот подход называется ILP (Instruction-Level Parallelism) — параллелизм на уровне инструкций.

Однако у ILP есть физические пределы: зависимости между инструкциями, размер окна внеочередного исполнения, задержки памяти — и, не в последнюю очередь, декодер команд.
Дешифратор x86 — это сложнейший, горячий и прожорливый узел процессора.

Представьте типичную задачу бэкенда: парсинг входящего JSON-пакета, фильтрацию массива или сложение двух числовых срезов:

Go
for i := 0; i < 1000; i++ {
    c[i] = a[i] + b[i]
}

Для процессора этот невинный цикл означает необходимость выбрать из памяти, декодировать, переименовать регистры и прогнать через конвейер минимум 3 000 – 4 000 инструкций:

  • прочитать a[i];
  • прочитать b[i];
  • выполнить сложение a + b;
  • записать результат в c[i];
  • инкрементировать счетчик i++;
  • сравнить с границей i < 1000;
  • выполнить условный переход JNE.

Повторять один и тот же бюрократический ритуал декодирования тысячу раз ради примитивного сложения — чудовищное расточительство кремниевых ресурсов.

Что, если сказать процессору:
«Вот тебе одна-единственная инструкция сложения. Но на входы подаются не два одиноких числа, а сразу два непрерывных массива по 16 чисел в каждом. Сложи их все параллельно за один электрический такт!»

Эта парадигма называется DLP (Data-Level Parallelism — Параллелизм на уровне данных).
А ее аппаратное воплощение в современных процессорах носит имя SIMD.


Что такое SIMD?

В 1966 году профессор Стэнфорда Майкл Флинн предложил фундаментальную классификацию компьютерных архитектур (таксономия Флинна).
По способу взаимодействия потока команд и потока данных компьютеры делятся на четыре класса:

flowchart TD
    subgraph SISD ["SISD (Скалярный процессор)"]
        direction TB
        Instr1["Одна инструкция ADD RAX, RBX"] --> ALU1["Скалярное ALU (64 бита)"]
        D_A1["Число A (64 бита)"] --> ALU1
        D_B1["Число B (64 бита)"] --> ALU1
        ALU1 --> Res1["Один результат C (64 бита)"]
    end

    subgraph SIMD ["SIMD (Векторный процессор)"]
        direction TB
        InstrV["Одна инструкция VPADDQ YMM0, YMM1, YMM2"] --> ALU_V["Широкое векторное ALU (256 бит)"]
        D_AV["Вектор A: [A0, A1, A2, A3]"] --> ALU_V
        D_BV["Вектор B: [B0, B1, B2, B3]"] --> ALU_V
        ALU_V --> ResV["Вектор ответов: [C0, C1, C2, C3]"]
    end

    classDef sisd fill:#1e293b,stroke:#f59e0b,stroke-width:1px,color:#f8fafc;
    classDef simd fill:#1e293b,stroke:#10b981,stroke-width:1px,color:#f8fafc;
    class SISD,Instr1,ALU1,D_A1,D_B1,Res1 sisd;
    class SIMD,InstrV,ALU_V,D_AV,D_BV,ResV simd;
  
  1. SISD (Single Instruction, Single Data):
    Классический скалярный процессор. Обычная инструкция ADD RAX, RBX берет одно 64-битное число, складывает его с другим 64-битным числом и выдает ровно один результат.
  2. SIMD (Single Instruction, Multiple Data):
    Векторный процессор. Одна векторная микрокоманда (например, VPADDQ) за один такт складывает целые массивы чисел, задействуя широкие параллельные сумматоры на кристалле.
  3. MISD (Multiple Instruction, Single Data):
    Редкая архитектура высокой надежности (космические аппараты, шаттлы NASA), где несколько независимых процессоров выполняют разные инструкции над одним потоком данных для перекрестной проверки сбоев.
  4. MIMD (Multiple Instruction, Multiple Data):
    Многоядерные процессоры и распределенные кластеры, где каждое ядро выполняет свою независимую программу над своими данными.

Под капотом: Векторные регистры

Как процессору удается удерживать и складывать столько чисел одновременно?

В статье 6. Анатомия CPU. Datapath, Control Unit и Register File мы изучили регистры общего назначения (GPR), такие как RAX или RSP. Их физическая ширина в архитектуре x86-64 строго ограничена 64 битами (8 байт).

Чтобы не ломать старый софт, инженеры добавили в ядро процессора параллельный векторный регистровый файл (Vector Register File) и независимые сверхширокие векторные ALU:

flowchart TD
    subgraph Evolution ["Эволюция ширины векторных регистров x86-64"]
        direction TB
        GPR["GPR (RAX, RBX): 64 бита (8 байт) - 1x int64"]
        XMM["XMM (SSE): 128 бит (16 байт) - 2x int64 / 4x int32 / 16x byte"]
        YMM["YMM (AVX2): 256 бит (32 байта) - 4x int64 / 8x int32 / 32x byte"]
        ZMM["ZMM (AVX-512): 512 бит (64 байта) - 8x int64 / 16x int32 / 64x byte (Размер Cache Line!)"]
    end

    classDef gpr fill:#131d33,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
    classDef xmm fill:#1e293b,stroke:#6366f1,stroke-width:1px,color:#f8fafc;
    classDef ymm fill:#1e293b,stroke:#10b981,stroke-width:1px,color:#f8fafc;
    classDef zmm fill:#0f172a,stroke:#f43f5e,stroke-width:1px,color:#f8fafc;
    class GPR gpr;
    class XMM xmm;
    class YMM ymm;
    class ZMM zmm;
  
  1. XMM регистры (SSE — Streaming SIMD Extensions):
    Ширина 128 бит (16 байт). В процессоре их 16 штук: от X0 до X15. В один регистр XMM помещается:
    • два числа int64 или float64;
    • четыре числа int32 или float32;
    • шестнадцать символов типа byte!
  2. YMM регистры (AVX / AVX2):
    Ширина 256 бит (32 байта). Нижняя половина каждого YMM-регистра прозрачно совпадает с соответствующим регистром XMM.
  3. ZMM регистры (AVX-512):
    Ширина 512 бит (64 байта). В один такой регистр за раз помещается ровно 64 байта — это размер стандартной строки кэша процессора (Cache Line)!

SIMD в Go: Механическая симпатия

В языках C, C++ и Rust компиляторы (Clang, GCC) оснащены агрессивными оптимизаторами-автовекторизаторами. Вы пишете обычный цикл for, включаете флаг -O3, и компилятор сам превращает его в SIMD-инструкции.

Но значит ли это, что Go работает медленно? Вовсе нет!
Вся стандартная библиотека Go пропитана SIMD-оптимизациями. Но написаны они вручную на ассемблере Plan 9 лучшими инженерами ядра Go.

Пакеты bytes (bytes.Equal, bytes.Index), strings, crypto (аппаратное шифрование AES-NI, SHA-extensions), хэш-таблицы (Swiss Tables в Go 1.24) работают с запредельной скоростью именно благодаря SIMD.

Как выглядит векторное сравнение байт на уровне железа

Взглянем на упрощённый фрагмент в духе ассемблерных функций пакета internal/bytealg (например, compare_amd64.s; bytes.Equal в современном Go сводится к сравнению строк string(a) == string(b), которое компилируется в вызов memequal), сравнивающий первые 16 байт двух слайсов:

Assembly
// AX указывает на слайс A, BX указывает на слайс B
MOVOU (AX), X0    // Загружаем 16 байт (128 бит) из слайса A в векторный регистр X0
MOVOU (BX), X1    // Загружаем 16 байт из слайса B в векторный регистр X1
PCMPEQB X1, X0    // Векторное сравнение (Packed Compare Equal Bytes)
PMOVMSKB X0, DX   // Извлекаем маску знаков: если все 16 байт совпали, DX = 0xFFFF
CMPL DX, $0xffff  // Одно сравнение вместо шестнадцати

Обратите внимание на красоту решения (в реальных функциях вокруг этого фрагмента есть цикл по блокам и обработка «хвоста» — здесь показано только ядро):

  • Нет побайтового цикла: 16 байт обрабатываются одной векторной операцией;
  • Нет ветвления на каждый байт: предсказателю переходов достаётся одна проверка на блок, а не шестнадцать;
  • Все 16 байт сравниваются одновременно одной инструкцией (PCMPEQB), а не шестнадцатью скалярными.

Использование SIMD в высоконагруженном бэкенде

Если ваш сервис занимается обработкой огромных массивов информации (In-Memory базы данных, парсинг логов, фильтрация сетевого трафика, поиск похожих векторов для AI-эмбеддингов), обычного скалярного Go будет недостаточно.

В экосистеме Go существуют признанные высокопроизводительные решения, выжимающие SIMD до предела:

  1. simdjson-go:
    Порт знаменитой библиотеки simdjson Даниэля Лемира. Она парсит гигабайты JSON в секунду! Алгоритм загружает в регистры 64 байта текста за раз и с помощью векторных масок мгновенно находит все спецсимволы: кавычки ", двоеточия, открывающие скобки {.
  2. Аналитические СУБД и TSDB (например, VictoriaMetrics, ClickHouse):
    Системы такого класса активно применяют SIMD для сканирования и фильтрации колонок. Но не все они написаны на Go: ClickHouse — это C++, а VictoriaMetrics (Go) использует собственный формат сжатия временных рядов (дельты, десятичное представление значений и zstd), а не Gorilla XOR.
  3. Векторный поиск (Vector Similarity / Embeddings):
    Вычисление косинусного расстояния (Cosine Similarity) между 1536-мерными векторами OpenAI на чистом Go может работать в разы медленнее, чем на ассемблере с использованием векторных инструкций FMA (Fused Multiply-Add) (точный множитель зависит от CPU и размерности).


Итог

  1. DLP (Data-Level Parallelism): альтернатива усложнению конвейера. Одна векторная инструкция управляет массивом параллельных сумматоров, применяя операцию к сотням бит данных одновременно.
  2. Таксономия Флинна: разделяет скалярные процессоры (SISD) и векторные вычислители (SIMD).
  3. Векторные регистры: специализированные широкие хранилища процессора — XMM (128 бит / 16 байт), YMM (256 бит / 32 байта) и ZMM (512 бит / 64 байта, точно под размер Cache Line).
  4. Реальность Go: компилятор Go не умеет автоматически векторизовать код при сборке (cmd/compile не имеет автовекторизатора). Векторная мощь стандартных пакетов (bytes, strings, crypto) реализована через рукописный ассемблер Plan 9 (PCMPEQB, MOVOU).
  5. Цена широкого SIMD: использование 512-битных регистров раздувает контекст потока ОС до 2.5 КБ (XSAVE при переключении потоков выполняет ядро, а не рантайм Go), а также может снижать частоту ядра, что в высококонкурентных бэкендах способно приводить к росту p99-латентности.

Широкие 512-битные регистры звучат потрясающе. Возникает логичный вопрос: почему инженеры не сделают регистры шириной 2048 или 4096 бит, чтобы обрабатывать целые мегабайты за такт?

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

В следующей лекции мы детально разберем эволюцию AVX и скрытые физические ловушки векторных вычислений: 15. AVX, AVX2, AVX-512 и ограничения SIMD.