Dev48
ЯЗЫК
  • О нас
  • Услуги
  • Индустрии
  • Технологии
  • Статьи
  • Контакты
Забронировать звонок
    Главная/Статьи/Platformo nezavisimyy simd v go
Dev48

© 2026 · All rights reserved.

Платформо-независимый SIMD в Go

Источник: Go

Платформо-независимый SIMD в Go

Источник: Go

В Go 1.27 добавлен экспериментальный API SIMD, не зависящий от платформы

25 сентября 2026 г.

Версии Go 1.26 и 1.27 включают экспериментальные API для операций SIMD (одна инструкция, множество данных). SIMD — это встроенная функция многих современных процессоров, которая позволяет программному обеспечению очень быстро выполнять одинаковые операции над векторами данных, например, складывать 8 пар значений float64 за одну инструкцию. Это может значительно ускорить множество ресурсоемких задач: от криптографии и обработки данных до ИИ. Фактически, сборщик мусора Green Tea в Go даже использует SIMD для ускорения сканирования памяти на предмет живых объектов.

До появления этих новых экспериментальных API единственным способом получить доступ к этой функциональности из Go было написание кода на ассемблере Go. Это имело смысл только для действительно критичных к производительности вычислительных ядер, а это означало, что значительная часть ПО, которое могло бы выиграть от SIMD, просто оставляла большую часть мощности процессора неиспользованной.

В Go 1.26 был представлен SIMD API для архитектуры amd64, а в Go 1.27 добавлены API для arm64 (а именно NEON) и wasm. Однако фундаментальная проблема SIMD API заключается в огромных различиях между платформами: не только в том, какие операции они поддерживают, но даже в том, как представляются векторы. Некоторые платформы предоставляют векторы фиксированного размера (обычно от 128 до 512 бит), в то время как на других размер вектора неизвестен во время сборки и должен запрашиваться при запуске программы. Чтобы обеспечить полный доступ ко всему спектру этих платформ, эти API находятся в архитектурно-зависимом пакете archsimd.

Но Go 1.27 идет дальше этих архитектурно-зависимых API и представляет экспериментальный, полностью переносимый, не зависящий от платформы и размера интерфейс SIMD, созданный по мотивам библиотеки Highway для C++. Цель состоит в том, чтобы поддерживать написание один раз близкого по производительности к ассемблеру кода «simd» на платформах с поддержкой SIMD, а также обеспечивать эффективную эмуляцию на тех платформах, которые (пока) не имеют поддержки SIMD. В настоящее время пакет simd поддерживает AVX, AVX2 и AVX512 на amd64, NEON на arm64 и SIMD-инструкции для wasm.

Мотивация: различия между архитектурами SIMD

Архитектуры SIMD различаются по нескольким параметрам. Некоторые предоставляют один фиксированный размер вектора (wasm, PowerPC и s390x, 128 бит). Некоторые предоставляют несколько фиксированных размеров векторов (amd64 с 128, 256 и 512 битами; loong64 с 128 и 256 битами). Riscv64 поддерживает векторы произвольного размера от 128 до 65536 бит, хотя длина ограничена степенями двойки. Arm64 поддерживает один фиксированный размер (128 бит, NEON) и один переменный размер (128–2048 бит, только степени двойки, SVE). На конкретном экземпляре определенной архитектуры определение того, какие размеры поддерживает данный экземпляр, требует проверки возможностей: это amd64, но AVX, AVX2 или AVX512? Это arm64, но NEON или SVE? Если SVE, то какого размера? Какой вариант SVE: SVE, SVE2 или SVE2.1?

Различные архитектуры SIMD по-разному обрабатывают маскирование векторов. Для векторов конструкцию «если-то-иначе» можно реализовать с помощью масок: выполнить операцию, но присвоить результат (или выполнить загрузку/сохранение) только там, где маска имеет значение «истина». Некоторые варианты SIMD не предоставляют масок; все операции выполняются над всеми элементами, а «маскирование» осуществляется с помощью векторных битовых масок и векторных булевых операций (wasm, AVX, AVX2, NEON). Некоторые предоставляют специальные регистры масок, где один бит управляет операциями над одним элементом вектора (AVX512 и RVV). Другие (SVE) выделяют один бит на байт вектора, но младший бит битов маски каждого элемента управляет маскированными операциями. AVX2 также поддерживает маскированные загрузки и сохранения, но с использованием обычной маски-вектора и управлением по старшему биту.

Третий источник различий — сами операции. Каждая архитектура предоставляет собственные примитивы для перестановки элементов вектора; некоторые требуют константных входных данных, другие поддерживают переменные. Различные архитектуры SIMD поддерживают разные криптографические операции. Даже базовая арифметика может иметь разную поддержку; например, в wasm отсутствуют сравнения для векторов 64-битных целых чисел. Даже для заданной длины вектора на определенной архитектуре поддержка инструкций зависит от «особенностей» (features), которые необходимо проверять.

Хотя архитектурно-зависимый пакет archsimd в Go был спроектирован так, чтобы быть максимально единообразным на разных архитектурах, многие из этих особенностей остаются и делают проектирование, написание и тестирование кода для мультиплатформенного SIMD сложной задачей. Мы могли бы сделать больше в пакете archsimd, чтобы разные архитектуры выглядели более похожими, но наши возможности ограничены, и дальнейшие шаги неизбежно привели бы к снижению эффективности.

Обзор

Новый пакет simd скрывает эти различия, исключая векторы фиксированного размера из системы типов и поддерживая только те операции, которые входят в пересечение возможностей всех различных платформ, а пробелы в этом пересечении заполняются эффективной эмуляцией на основе других SIMD-инструкций. Цель состоит в создании набора операций, который:

  • достаточен для поддержки многих алгоритмов обработки данных, выиграющих от векторизованной реализации (но не привязанных к конкретному размеру вектора);
  • эффективен так же, как язык ассемблера, когда операции исходного кода соответствуют базовому оборудованию;
  • в остальных случаях эмулируется наилучшим из возможных способов;
  • легко читается и понимается (даже/особенно если код в итоге напишет языковая модель).

На платформах, где отсутствуют SIMD-инструкции или нет поддержки в archsimd, все операции эмулируются, поэтому код, написанный с использованием пакета simd, будет работать всегда.

Чтобы использовать этот экспериментальный пакет, установите GOEXPERIMENT=simd, точно так же, как при использовании экспериментального пакета archsimd.

Типы векторов в пакете simd представляют собой просто написанные с большой буквы во множественном числе примитивные типы, например, simd.Uint8s или simd.Float32s. Векторы загружаются из срезов и сохраняются в них, например:

Этот пример также демонстрирует одно из ограничений первого экспериментального выпуска этого пакета: поскольку не существует общего способа суммирования всех элементов вектора, он не поддерживается simd в Go 1.27, хотя ReduceSum появится в следующем релизе, так что сумму можно будет заменить просто на simd.ReduceSum.

Сравнения SIMD создают значения масок, которые соответствуют ширине соответствующих элементов векторов, так что сравнения Int8s дают Mask8s и т. д., а значения масок могут использоваться для выбора и фильтрации векторов.

Поддерживаемые операции пакета simd в Go 1.27

В этой таблице V и U — векторные типы, M — тип маски, E — скалярный тип, а W — ширина.

Функции загрузки / широковещательной рассылки на уровне пакета

Операции сохранения / со строками

Арифметические операции

Логические операции и операции маскирования векторов

Операции сравнения

Операции преиобразования

Методы масок

Операции сдвига и вращения

Операции изменения формы с нулевой стоимостью

Переход к платформо-специфичному коду / из него

Может случиться так, что пакет simd окажется слишком ограниченным для всех частей конкретного приложения или что мы еще не предоставили адекватную эмуляцию для какой-то необходимой функции. В этом случае пакет simd поддерживает переход к архитектурно-зависимым SIMD и обратно. Каждый векторный тип в пакете simd имеет метод конвертации ToArch(), возвращающий значение типа any. Для этого значения можно выполнить приведение типов к одному из архитектурно-зависимых типов для конкретной платформы. Чтобы выполнить обратное преобразование, используйте одну из функций simd.<SimdType>FromArch. Для переносимого кода это накладывает обязательство написать архитектурно-зависимый код для каждой из платформ, включая эмуляцию.

Вот полный пример для метода или функции, которая в настоящее время отсутствует, но должна быть добавлена в Go 1.28. Предположим, вашему алгоритму требуется Int8s.OnesCount() (которой нет в simd в Go 1.27). Вместо переписывания всего алгоритма для каждой платформы можно просто реализовать недостающую операцию.

Сначала для amd64, в котором отсутствует инструкция для AVX и AVX2, но не для AVX512:

Преобразование интерфейса и переключение типов выглядят неэффективно, однако реализация simd на стороне компилятора специализирует код и оптимизирует переключение типов, удаляя его.

NEON и Wasm поддерживают Int8s.OnesCount(), поэтому их реализация гораздо проще, хотя в ней все равно используются Int8s.ToArch и Int8sFromArch.

Не забывайте, что у некоторых людей нет аппаратной поддержки SIMD:

И в завершение примера — отдельная функция эмуляции, используемая в качестве резервного варианта во всех реализациях:

Пересечение API и эмуляция методов

Любые операции, предлагаемые пакетом simd, должны выполняться с приемлемой производительностью на большинстве архитектур. В качестве первого шага любая операция, поддерживаемая везде, может быть легко поддержана в simd. Обычно это загрузки, выгрузки, арифметика и сравнения (но не все сравнения!).

Наивное пересечение методов SIMD с разных архитектур все равно оставляет множество пробелов. Они заполняются путем добавления эмуляций в различные архитектурно-зависимые API archsimd. Эти API уже содержат множество тривиальных эмуляций, чтобы облегчить жизнь программистам на Go; операции сложения со знаком и без знака используют одну и ту же инструкцию, но точно так же, как Go поддерживает оператор + как для int, так и для uint, пакет archsimd предоставляет и Int8x16.Add(Int8x16), и Uint8x16.Add(Uint8x16), даже если они компилируются в одну и ту же инструкцию. Современные языки программирования также не ожидают от программистов знаний о том, как реализовать отрицание с плавающей запятой и абсолютное значение с помощью манипуляций с битами, поэтому archsimd реализует это там, где необходимо, или «эмулирует», если посмотреть на это с определенной стороны.

Существует множество эмуляций, требующих всего 2 или 3 инструкций; например, некоторые архитектуры поддерживают одинаковое расстояние сдвига для всех элементов вектора, в то время как другие поддерживают разное расстояние сдвига для каждого элемента вектора. Чтобы поддержать скалярный сдвиг в simd, мы эмулируем скалярный сдвиг с помощью векторного. В некоторых архитектурах отсутствуют некоторые сравнения без знака — они заменяются сравнением со знаком плюс двумя операциями XOR с константой.

Не все недостающие инструкции так просты. Инструкция «бескассового умножения» (carryless multiply) важна для криптографии и вычисления контрольных сумм CRC, но она поддерживается не всегда. Исключение ее из API simd помешало бы ее использованию в некоторых важных алгоритмах. Поэтому мы предоставляем эмуляцию, и поскольку одним из важных вариантов использования является криптография, время ее выполнения не зависит от входных данных.

В других случаях вместо реализации примитивной инструкции вроде «сложения пар» (также называемой «горизонтальным сложением») для пакета simd в следующем релизе мы предоставим высокоуровневую операцию, для которой обычно используется сложение пар, а именно редукцию суммы. Это также помогает защитить пользователей от зависимости от длины векторов; даже при наличии аппаратной инструкции для сложения пар количество шагов редукции зависит от длины вектора.

Ограничение по поддержке всех платформ, включая те, которые, по нашим прогнозам, появятся в archsimd в течение следующего года или около того, заставляет использовать несколько консервативный подход к выбору методов, добавляемых в simd. Riscv64, ppc64, s390x и loong64 имеют собственные расширения SIMD.

Настройки GODEBUG

На платформах, где имеется некоторая аппаратная поддержка, поведение можно изменить с помощью GODEBUG, чтобы упростить тестирование кода, использующего simd, с различными конфигурациями оборудования. В Go 1.27 уровни поддержки SIMD примерно описываются длиной вектора:

  • GODEBUG=simd=0 означает использование эмуляции для операций SIMD, даже если доступна аппаратная поддержка.
  • GODEBUG=simd=128 означает использование 128-битных векторов и их возможностей. Если возможности недоступны, немедленно вызывается паника.
  • GODEBUG=simd=256 означает использование 256-битных векторов и их возможностей, если это возможно.
  • GODEBUG=simd=512 означает использование 512-битных векторов и их возможностей, если это возможно.
  • GODEBUG=simd=+128 означает использование 128-битных векторов и их возможностей, даже если некоторые функции не поддерживаются. Если используются неподдерживаемые инструкции, код вызовет панику, но если нет — он все равно может выполниться. Примером этого является Raspberry Pi, которая поддерживает NEON, но не имеет PMULL (бескассовое умножение).
  • GODEBUG=simd=+256 означает использование 256-битных векторов и их возможностей, даже если некоторые функции не поддерживаются. Если используются неподдерживаемые инструкции, код вызовет панику, но если нет — он все равно может выполниться. Примером этого является эмуляция amd64 в Apple Silicon, которая поддерживает AVX2, но не VPCLMULQDQ (опять же, бескассовое умножение).
  • GODEBUG=simd=+512 означает использование 512-битных векторов, даже если некоторые функции не поддерживаются.

Детали реализации

Если вы отлаживаете код, использующий simd, или просто просматриваете трассировку стека, вы заметите несколько странных дополнительных типов и методов. Причина в том, что simd — это одновременно пакет, внутренний пакет реализации и результат переписывания AST во фронтенде компилятора.

Переписывание AST создает несколько специализированных копий функций, переменных и типов, в которых упоминаются типы simd, причем типы simd заменяются на ссылки на специализированные по размеру типы в simd/internal/bridge. Каждый из этих мостовых типов определяется как тип archsimd, но с ограниченным набором методов. Специализированные функции, переменные и типы получают суффикс вида @simdNNN, где NNN — это либо длина вектора (128, 256 или 512), либо 0, что указывает на эмуляцию. Функции, которые упоминают simd внутри себя, но не в своей сигнатуре, преобразуются в обертки (wrapper), которые выполняют переключение в зависимости от уровня SIMD, обнаруженного при запуске программы, и вызывают соответствующую специализированную версию этой функции. Специализированные функции вызывают другие специализированные функции напрямую без накладных расходов на диспетчеризацию (и, возможно, с инлайнингом). Эта стратегия перезаписи была выбрана как компромисс между дублированием кода и производительностью SIMD; накладные расходы поднимаются настолько высоко, насколько это необходимо, чтобы избежать диспетчеризации внутри вычислений SIMD, но не выше. Если диспетчеризация SIMD оказывается «слишком низкой» в вычислениях, фиктивное упоминание типа simd переместит ее вверх, как в этом примере:

Что нас ждет в будущем

Мы планируем в ближайшее время опубликовать запись в блоге, в которой более подробно описывается archsimd.

В Go 1.28 мы намерены добавить поддержку SVE в archsimd, а также надеемся добавить её в simd. Что еще более важно, мы надеемся расширить набор SIMD-операций, уже поддерживаемых пакетом simd (например, OnesCount, операции с масками, операции редукции, операции перестановки векторов). Go 1.28 также будет включать небольшое количество «вариантов функций», чтобы избежать полного перехода на эмуляцию для платформ, у которых есть аппаратная реализация векторов, но отсутствует всего одна или несколько операций, таких как Raspberry Pi.

← Все статьи