[Для основных данных строки SIMD-доступ к строке быстрый, но к столбцу медленный]
Да, это характер x86-64 и подобных архитектур,Доступ к последовательным данным в памяти быстрый, но доступ к разрозненным данным (случайным или регулярным образом) медленный.Это является следствием наличия кэшей процессора.
Существует два основных подхода: копировать данные в новый порядок, который облегчает более эффективные шаблоны доступа, или вы выполняете вычисления в порядке, который позволяет более совершенные шаблоны доступа.
Нет, нет никаких правил большого пальца или золотых уловок, которые заставляют все просто работать.На самом деле, даже сравнение различных реализаций является сложным, потому что существует очень много сложных взаимодействий (от задержек кэша до чередования операций, к кешу и шаблонам доступа к памяти), что результаты сильно зависят от конкретного оборудования и набора данных под рукой.
Давайте рассмотрим типичный пример умножения матрицы на матрицу.Допустим, мы умножаем две матрицы 5 × 5 (c = a × b), используя стандартный порядок данных в мажорной строке C:
c00 c01 c02 c03 c04 a00 a01 a02 a03 a04 b00 b01 b02 b03 b04
c05 c06 c07 c08 c09 a05 a06 a07 a08 a09 b05 b06 b07 b08 b09
c10 c11 c12 c13 c14 = a10 a11 a12 a13 a14 × b10 b11 b12 b13 b14
c15 c16 c17 c18 c19 a15 a16 a17 a18 a19 b15 b16 b17 b18 b19
c20 c21 c22 c23 c24 a20 a21 a22 a23 a24 b20 b21 b22 b23 b24
Если мы запишем результат как вертикальные векторные регистры SIMD с пятью компонентами, мыиметь
c00 a00 b00 a01 b05 a02 b10 a03 b15 a04 b20
c01 a00 b01 a01 b06 a02 b11 a03 b16 a04 b21
c02 = a00 × b02 + a01 × b07 + a02 × b12 + a03 × b17 + a04 × b22
c03 a00 b03 a01 b08 a02 b13 a03 b18 a04 b23
c04 a00 b04 a01 b09 a02 b14 a03 b19 a04 b24
c05 a05 b00 a06 b05 a07 b10 a08 b15 a09 b20
c06 a05 b01 a06 b06 a07 b11 a08 b16 a09 b21
c07 = a05 × b02 + a06 × b07 + a07 × b12 + a08 × b17 + a09 × b22
c08 a05 b03 a06 b08 a07 b13 a08 b18 a09 b23
c09 a05 b04 a06 b09 a07 b14 a08 b19 a09 b24
и так далее.Другими словами, если c
имеет тот же порядок, что и b
, мы можем использовать регистры SIMD с последовательным содержимым памяти для c
и b
и собирать только a
.Кроме того, в SIMD-регистрах для a
все компоненты имеют одинаковое значение.
Обратите внимание, однако, что регистры b
повторяются для всех пяти строк c
.Таким образом, может быть лучше инициализировать c
нулем, а затем выполнять добавления с продуктами, имеющими такие же регистры b
SIMD:
c00 a00 b00 c05 a05 b00 c10 a10 b00 c15 a15 b00 c20 a20 b00
c01 a00 b01 c06 a05 b01 c11 a10 b01 c16 a15 b01 c21 a20 b01
c02 += a00 × b02, c07 += a05 × b02, c12 += a10 × b02, c17 += a15 × b02, c22 += a20 × b02
c03 a00 × b03 c08 a05 b03 c13 a10 b03 c18 a15 b03 c23 a20 b03
c04 a00 × b04 c09 a05 b04 c14 a10 b04 c19 a15 b04 c24 a20 b04
Если сначала мы транспонировали a
, то вектор SIMDрегистры для a
также будут получать значения из последовательных ячеек памяти.На самом деле, если a
достаточно велико, линеаризация шаблона доступа к памяти для a
также дает достаточно большой прирост скорости, что позволяет быстрее выполнять транспонирование (используя uint32_t
для операций с плавающей запятой и uint64_t
для операций с двойными числами;т.е. вообще не использовать SIMD или с плавающей запятой для транспонирования, а просто копировать хранилище в порядке транспонирования).
Обратите внимание, что ситуация с порядком данных по главному столбцу, т. е. порядком данных, транспонированным по сравнению с приведенным выше, оченьаналогичный.Здесь глубокая симметрия.Например, если c
и b
имеют одинаковый порядок данных, а a
противоположный порядок данных, вы можете SIMD-векторизовать матричный продукт эффективно без необходимости копировать какие-либо данные.Различается только суммирование, поскольку это зависит от порядка данных, и умножение матриц не является коммутативным (a × b! = B × a).
Очевидно, что основная проблема заключается в том, что векторные регистры SIMD имеют фиксированныйsize, поэтому вместо использования полной строки в качестве регистра, как в примере выше, вы можете использовать только частичные строки.(Если количество столбцов в результате не кратно ширине регистра SIMD, у вас также должен быть этот частичный вектор.)
SSE и AVX имеют относительно большое количество регистров (8,16 или 32, в зависимости от набора используемых расширений) и в зависимости от конкретного типа процессора, может быть в состоянии выполнять некоторые векторные операции одновременно или, по меньшей мере, с меньшим временем ожидания, если чередуются несвязанные векторные операции.Таким образом, даже выбор ширины блока для одновременной работы и того, является ли этот блок похожим на расширенный вектор или более похожим на блочную подматрицу, зависит от обсуждения, тестирования и сравнения.
Так как мне сделать матрично-матричное умножение наиболее эффективно с использованием SIMD?
Как я уже сказал, это зависит от набора данных.Боюсь, нелегких ответов.
Основными параметрами (для выбора наиболее эффективного подхода) являются размеры и порядок в памяти матриц мультипликатора и результата.
Будет еще интереснее, если вы рассчитаете произведение более двух матриц разных размеров.Это потому, что количество операций зависит от порядка продуктов.
Почему вы так обескураживаете?
На самом деле это не так.Все вышеперечисленное означает, что не так уж много людей могут справиться с подобными сложностями и оставаться в здравом уме и продуктивно, поэтому существует множество неизученных подходов и много чего можно добиться в реальных условиях.
Даже еслимы игнорируем встроенные компиляторы SIMD (в данном случае <x86intrin.h>
), мы можем применять приведенную выше логику при проектировании внутренних структур данных, так что используемый нами компилятор C имеет наилучшие возможности для векторизации вычислений для нас.(Они пока не очень хороши в этом. Как я уже сказал, сложные вещи. Некоторым нравится Fortran лучше, чем C, потому что его выражения и правила упрощают их компилятору и оптимизируют их.)
Если бы это было просто или легко, решения были бы хорошо известны.Но это не так, потому что это не так.Но это не значит, что это невозможно или вне нашей досягаемости;все, что это означает, - то, что достаточно умные разработчики еще не приложили достаточно усилий, чтобы разгадать это.