Я ищу более быстрый и сложный способ умножить две матрицы 4x4 на C. В моем текущем исследовании сосредоточена сборка x86-64 с расширениями SIMD. До сих пор я создал функцию, которая примерно в 6 раз быстрее, чем наивная реализация C, которая превзошла мои ожидания по улучшению производительности. К сожалению, это остается верным только тогда, когда флаги оптимизации не используются для компиляции (GCC 4.7). С -O2
C становится быстрее, и мои усилия становятся бессмысленными.
Я знаю, что современные компиляторы используют сложные методы оптимизации для достижения почти идеального кода, обычно быстрее, чем гениальный кусок сборки с ручным суффиксом. Но в меньшинстве критически важных дел человек может попытаться бороться за такт с компилятором. Особенно, когда некоторые математики, поддерживаемые современной ISA, могут быть исследованы (как в моем случае).
Моя функция выглядит следующим образом (синтаксис AT & T, GNU Assembler):
.text
.globl matrixMultiplyASM
.type matrixMultiplyASM, @function
matrixMultiplyASM:
movaps (%rdi), %xmm0 # fetch the first matrix (use four registers)
movaps 16(%rdi), %xmm1
movaps 32(%rdi), %xmm2
movaps 48(%rdi), %xmm3
xorq %rcx, %rcx # reset (forward) loop iterator
.ROW:
movss (%rsi), %xmm4 # Compute four values (one row) in parallel:
shufps $0x0, %xmm4, %xmm4 # 4x 4FP mul's, 3x 4FP add 6x mov per row,
mulps %xmm0, %xmm4 # expressed in four sequences of 5 instructions,
movaps %xmm4, %xmm5 # executed 4 times for 1 matrix multiplication.
addq $0x4, %rsi
movss (%rsi), %xmm4 # movss + shufps comprise _mm_set1_ps intrinsic
shufps $0x0, %xmm4, %xmm4 #
mulps %xmm1, %xmm4
addps %xmm4, %xmm5
addq $0x4, %rsi # manual pointer arithmetic simplifies addressing
movss (%rsi), %xmm4
shufps $0x0, %xmm4, %xmm4
mulps %xmm2, %xmm4 # actual computation happens here
addps %xmm4, %xmm5 #
addq $0x4, %rsi
movss (%rsi), %xmm4 # one mulps operand fetched per sequence
shufps $0x0, %xmm4, %xmm4 # |
mulps %xmm3, %xmm4 # the other is already waiting in %xmm[0-3]
addps %xmm4, %xmm5
addq $0x4, %rsi # 5 preceding comments stride among the 4 blocks
movaps %xmm5, (%rdx,%rcx) # store the resulting row, actually, a column
addq $0x10, %rcx # (matrices are stored in column-major order)
cmpq $0x40, %rcx
jne .ROW
ret
.size matrixMultiplyASM, .-matrixMultiplyASM
Он вычисляет весь столбец результирующей матрицы на итерацию, обрабатывая четыре поплавков, упакованных в 128-разрядные регистры SSE. Полная векторизация возможна с помощью бит математического (переупорядочения операций и агрегации) и инструкций mullps
/addps
для параллельного умножения/добавления пакетов 4xfloat. Кодирует повторные регистры, предназначенные для передачи параметров (%rdi
, %rsi
, %rdx
: GNU/Linux ABI), извлекает выгоду из (внутреннего) цикла разворачивания и сохраняет одну матрицу полностью в регистрах XMM, чтобы уменьшить считывание памяти. A вы можете видеть, я исследовал тему и потратил свое время, чтобы реализовать ее, насколько я могу.
Наивный вычисление C, выполняющее мой код, выглядит следующим образом:
void matrixMultiplyNormal(mat4_t *mat_a, mat4_t *mat_b, mat4_t *mat_r) {
for (unsigned int i = 0; i < 16; i += 4)
for (unsigned int j = 0; j < 4; ++j)
mat_r->m[i + j] = (mat_b->m[i + 0] * mat_a->m[j + 0])
+ (mat_b->m[i + 1] * mat_a->m[j + 4])
+ (mat_b->m[i + 2] * mat_a->m[j + 8])
+ (mat_b->m[i + 3] * mat_a->m[j + 12]);
}
Я исследовал оптимизированный сборный вывод вышеуказанного кода C, который, сохраняя поплавки в XMM-регистрах, не включает никаких параллельных операций - просто скалярных вычислений, арифметических указателей и условных переходов. Код компилятора представляется менее преднамеренным, но он по-прежнему немного эффективнее, чем моя векторная версия, которая, как ожидается, будет примерно в 4 раза быстрее. Я уверен, что общая идея правильная - программисты делают подобные вещи с полезными результатами. Но что здесь не так? Есть ли какие-либо проблемы с распределением регистров или инструкциями, о которых я не знаю? Знаете ли вы какие-либо инструменты сборки или трюки x86-64 для поддержки моей битвы с машиной?