Подтвердить что ты не робот

Оптимизация x64 ассемблера MUL loop

Я пишу математический код, который должен быстро умножать большие числа. Он разбивается на умножения массива целых чисел с одним целым числом. В С++ это выглядит так (на unsigned):

void muladd(unsigned* r, const unsigned* a, unsigned len, unsigned b) {
   unsigned __int64 of = 0;  // overflow
   unsigned i = 0;  // loop variable
   while (i < len) {
      of += (unsigned __int64)a[i] * b + r[i];
      r[i] = (unsigned)of;
      of >>= 32;
      ++i;
   }
   r[i] = (unsigned)of;  // save overflow
}

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

mov   rax, rdi                             ; rdi = b
mul   QWORD PTR [rbx+r10*8-64]             ; rdx:rax = a[i] * b; r10 = i
mov   rsi, QWORD PTR [r14+r10*8-64]        ; r14 = r; rsi = r[i]
add   rax, rsi
adc   rdx, 0
add   rax, r11                             ; r11 = of (low part)
adc   rdx, 0
mov   QWORD PTR [r14+r10*8-64], rax        ; save result
mov   r11, rdx

; this repeats itself 8 times with different offsets

Когда я сравниваю это, я нахожу, что для моего Core2 Quad требуется около 6,3 циклов при умножении на умножение.

Мой вопрос: могу ли я как-то ускорить это? К сожалению, я не вижу возможности избежать одного из добавлений, и для умножения всегда нужен RDX: RAX, поэтому мне нужно перемещать данные и не может сортировать "размножаться параллельно".

Любые идеи кто-нибудь?

Update: После некоторого тестирования мне удалось довести скорость до примерно 5,4 циклов на 64-битный MUL (включая все накладные расходы, перемещение и цикл). Я думаю, что это самое лучшее, что вы можете получить на Core2, поскольку Core2 не имеет очень быстрой инструкции MUL: он имеет пропускную способность 3 и латентность 6 (соответственно 7) циклов. Песчаный мост будет намного лучше с пропускной способностью 1 и латентностью 3 (соответственно 4) цикла.

Что касается гораздо меньшего числа для GMP: я получил это из своего исходного кода, и мне кажется, что это теоретическое число. Но то, что является уверенным в том, что это число, которое было рассчитано для процессора AMD K9. И из того, что я прочитал, я понял, что у AMD есть более быстрая единица MUL, чем у более старых чипов Intel.

4b9b3361

Ответ 1

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

Я бы попытался выполнить предварительную выборку [i] и r [i]

если gcc использует функцию __builtin_prefetch() или инструкцию PREFETCHT0 в ассемблере

http://gcc.gnu.org/onlinedocs/gcc-3.3.6/gcc/Other-Builtins.html

Когда это работает, результаты могут быть значительными. До тех пор, пока цикл длится тысячу или около итераций, я предварительно выбрал [i + 64] и r [i + 64] в качестве отправной точки и посмотрю, какая разница в вашем процессоре. Возможно, вам придется попробовать большие расстояния перед выборкой.

Ответ 2

Похоже, ваша рутина может выиграть от SSE. PMULLD и PADDD выглядят как соответствующие инструкции. Не знаете, почему ваш компилятор не производит SSE.

Ответ 3

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

Ответ 4

Есть ли какое-либо важное значение перед вызовом?

Если это так, и вы накапливаете на нем, то прекратите читать сейчас.

Если это не так (т.е. вы всегда накапливаетесь на нули) и предполагаете, что вы вызываете эту функцию на массивах, значительно больших размеров кеша, тогда я бы искал способ устранить необходимость читать из r и преобразовать "сохранить результат" MOV в MOVNT (_mm_stream_ps в intrinsics).

Это может значительно повысить производительность. Как? В настоящее время ваши кэши извлекают строки кэша из строки, получая строки кэша из r и записывая строки кэша обратно в r. С так называемыми потоковыми хранилищами вы просто извлекаете строки кэша из строки и записываете прямо в r: гораздо меньше трафика шины. Если вы посмотрите на любую современную реализацию emc CRT, она переключится на использование потоковых хранилищ выше некоторого порога, связанного с размером кеша (и запустите почти в два раза быстрее, чем memcpy, используя обычные ходы).