Каковы эти дополнительные инструкции по разборке при использовании встроенных SIMD?
Я тестирую, какое ускорение я могу получить, используя инструкции SIMD с RyuJIT, и вижу некоторые инструкции по разборке, которые я не ожидаю. Я основываю код на этом посте в блоге от Кевина Фрея из команды RyuJIT и на соответствующем посте здесь. Вот функция:
static void AddPointwiseSimd(float[] a, float[] b) {
int simdLength = Vector<float>.Count;
int i = 0;
for (i = 0; i < a.Length - simdLength; i += simdLength) {
Vector<float> va = new Vector<float>(a, i);
Vector<float> vb = new Vector<float>(b, i);
va += vb;
va.CopyTo(a, i);
}
}
Раздел разборки, который я запрашиваю, копирует значения массива в Vector<float>
, Большая часть разборки аналогична той, что была в сообщениях Кевина и Саши, но я выделил некоторые дополнительные инструкции (вместе с моими запутанными аннотациями), которых нет в их разборках:
;// Vector<float> va = new Vector<float>(a, i);
cmp eax,r8d ; <-- Unexpected - Compare a.Length to i?
jae 00007FFB17DB6D5F ; <-- Unexpected - Jump to range check failure
lea r10d,[rax+3]
cmp r10d,r8d
jae 00007FFB17DB6D5F
mov r11,rcx ; <-- Unexpected - Extra register copy?
movups xmm0,xmmword ptr [r11+rax*4+10h ]
;// Vector<float> vb = new Vector<float>(b, i);
cmp eax,r9d ; <-- Unexpected - Compare b.Length to i?
jae 00007FFB17DB6D5F ; <-- Unexpected - Jump to range check failure
cmp r10d,r9d
jae 00007FFB17DB6D5F
movups xmm1,xmmword ptr [rdx+rax*4+10h]
Обратите внимание, что проверка диапазона цикла соответствует ожидаемой:
;// for (i = 0; i < a.Length - simdLength; i += simdLength) {
add eax,4
cmp r9d,eax
jg loop
так что я не знаю, почему есть дополнительные сравнения с eax
, Может кто-нибудь объяснить, почему я вижу эти дополнительные инструкции и возможно ли от них избавиться.
В случае, если это связано с настройками проекта, у меня есть очень похожий проект, который показывает ту же проблему здесь, на github (см. FloatSimdProcessor.HwAcceleratedSumInPlace()
или же UShortSimdProcessor.HwAcceleratedSumInPlaceUnchecked()
).
1 ответ
Я буду комментировать генерацию кода, которую я вижу, для процессора, поддерживающего AVX2, такого как Haswell, он может перемещать 8 операций с плавающей точкой за раз:
00007FFA1ECD4E20 push rsi
00007FFA1ECD4E21 sub rsp,20h
00007FFA1ECD4E25 xor eax,eax ; i = 0
00007FFA1ECD4E27 mov r8d,dword ptr [rcx+8] ; a.Length
00007FFA1ECD4E2B lea r9d,[r8-8] ; a.Length - simdLength
00007FFA1ECD4E2F test r9d,r9d ; if (i >= a.Length - simdLength)
00007FFA1ECD4E32 jle 00007FFA1ECD4E75 ; then skip loop
00007FFA1ECD4E34 mov r10d,dword ptr [rdx+8] ; b.Length
00007FFA1ECD4E38 cmp eax,r8d ; if (i >= a.Length)
00007FFA1ECD4E3B jae 00007FFA1ECD4E7B ; then OutOfRangeException
00007FFA1ECD4E3D lea r11d,[rax+7] ; i+7
00007FFA1ECD4E41 cmp r11d,r8d ; if (i+7 >= a.Length)
00007FFA1ECD4E44 jae 00007FFA1ECD4E7B ; then OutOfRangeException
00007FFA1ECD4E46 mov rsi,rcx ; move a[i..i+7]
00007FFA1ECD4E49 vmovupd ymm0,ymmword ptr [rsi+rax*4+10h]
00007FFA1ECD4E50 cmp eax,r10d ; same as above
00007FFA1ECD4E53 jae 00007FFA1ECD4E7B ; but for b
00007FFA1ECD4E55 cmp r11d,r10d
00007FFA1ECD4E58 jae 00007FFA1ECD4E7B
00007FFA1ECD4E5A vmovupd ymm1,ymmword ptr [rdx+rax*4+10h]
00007FFA1ECD4E61 vaddps ymm0,ymm0,ymm1 ; a[i..] + b[i...]
00007FFA1ECD4E66 vmovupd ymmword ptr [rsi+rax*4+10h],ymm0
00007FFA1ECD4E6D add eax,8 ; i += 8
00007FFA1ECD4E70 cmp r9d,eax ; if (i < a.Length)
00007FFA1ECD4E73 jg 00007FFA1ECD4E38 ; then loop
00007FFA1ECD4E75 add rsp,20h
00007FFA1ECD4E79 pop rsi
00007FFA1ECD4E7A ret
Таким образом, eax сравнивает те "надоедливые связанные проверки", о которых говорится в посте блога. Сообщение в блоге дает оптимизированную версию, которая на самом деле не реализована (пока), реальный код прямо сейчас проверяет как первый, так и последний индекс из 8-ми float, которые перемещаются одновременно. Комментарий к сообщению в блоге "Надеемся, что мы добьемся достаточного усиления нашей работы по проверке границ" - это незавершенная задача:)
mov rsi,rcx
Инструкция также присутствует в сообщении блога и является ограничением в распределителе регистра. Вероятно, под влиянием того, что RCX является важным регистром, он обычно хранит это. Я полагаю, что недостаточно важного, чтобы выполнить эту работу, чтобы оптимизировать ее, перемещения между регистрами занимают 0 циклов, поскольку они влияют только на переименование регистров.
Обратите внимание, что разница между SSE2 и AVX2 ужасна, хотя код перемещается и добавляет 8 операций с плавающей точкой за раз, фактически он использует только 4 из них. Vector<float>.Count
4 независимо от вкуса процессора, оставляя 2x перфоратора на столе. Трудно скрыть детали реализации, я думаю.