Contenuto
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, который выдавал некорректные результаты