ARM, который мы заслужили



Когда люди только в последнее десятилетие смотрели на SIMD x86_64, параллельно развивался AArch64, на который очень мало обращали внимания. Сейчас обращают больше и хочется рассказать одну историю, о которой редко говорят. И вообще, про ARM никто не пишет, надо исправляться!



У AArch64 намного более серьёзный instruction set, когда дело доходит до conditional moves. В x86_64 есть cmov, когда вы двигаете регистр в другой в зависимости от результатов предущего сравнения



cmp %rax, %rcx

cmovzq $REG1, $REG2 # move reg1 into reg2 if flag ZF was set





И в целом всё, есть только cmov. А вот в AArch64 аналогом является csel, conditional select.



cmp $something1, $something2

csel $reg1, $reg2, $reg3, oper # reg1 = oper ? reg2 : reg3





oper может быть равно eq, ne и так далее. То есть csel if equal, csel if not equal и так далее.



Но помимо conditional select есть ещё многие другие, такие как conditional select increase/inverse/negate:



csinc $reg1, $reg2, $reg3, oper # reg1 = oper ? reg2 : (reg3 + 1)

csinv $reg1, $reg2, $reg3, oper # reg1 = oper ? reg2 : ~reg3

csneg $reg1, $reg2, $reg3, oper # reg1 = oper ? reg2 : -reg3





Но по мне самая недооцененная иструкция это conditional compare, дада, выглядит оно так



cmp $something1, $something2   

ccmp $reg1, $reg2, $flags, oper # flags = oper ? (cmp $reg1, $reg) : $flags





То есть можно манипулировать флагами сравнений после cmp через и таким образом проносить флаги дальше. Скажем, чтобы сравнить 16 байт между собой можно сделать



ldp x3, x4, [src1] # load 8 byte into x3, load 8 byte into x4

ldp x5, x6, [src2] # load 8 byte into x5, load 8 byte into x6

cmp x3, x5 # compare x3 and x5

ccmp x4, x6, 0, eq # if eq, compare x4, x6, otherwise flags are 0

b.ne L_SOMEWHERE # if flags are zero, branch





Такой трюк можно использовать, чтобы проносить результаты сравнений. Учитывая то, что ccmp занимает 1 цикл на Neoverse-n1 как и cmp, то это помогает сравнивать регионы памяти с тем же ptest/movmask на x86.



Вообще одно из самых сложных отличий SIMD x86 и ARM Neon заключается в movemask инструкции. Если коротко, для 16 байт оно забирает верхний бит каждого байта (если суффикс b) и обычно используется так



pcmpeqb %xmm1, %xmm2 # compare 2 regions, those who are equal, set to 0xff, otherwise 0

pmovmskb %xmm2, %ecx # move high bits, those who are equal, set to 1

cmp %ecx, 0xffff # compare

jne L_SOMEWHERE # jump





На Arm такой инструкции нет и эмулируется 3-4 инструкциями. Это огромная боль, когда ты хочешь мигрировать одно на другое, теряешь много перфа.



В итоге это один трюк как на AArch64 за примерно такое же количество циклов сравнивать регионы памяти.



Можете оценить сколько movemask используется в ClickHouse [2] -- дада, я сделал огромный issue, чтобы наконец-то починить все перформанс проблемы на AArch64.



В след раз расскажу ещё пару трюков, так как через movemask можно ещё находить первый несовпавший бит в коде выше через всякие count trailing zeros, и текущий трюк уже не работает.



Полезные ссылки.



[1] Этот трюк используется в memcmp в glibc

[2] Можете оценить сколько movemask используется в ClickHouse

[3] Оптимизации через csinc в Google Snappy

[4] Neoverse-N1 optimization guide

[5] Movemask на x86

[6] Как я чинил movemask на AArch64 в Vectorscan, который выдавал некорректные результаты