ARM, который мы заслужили
Когда люди только в последнее десятилетие смотрели на SIMD x86_64, параллельно развивался AArch64, на который очень мало обращали внимания. Сейчас обращают больше и хочется рассказать одну историю, о которой редко говорят. И вообще, про ARM никто не пишет, надо исправляться!
У AArch64 намного более серьёзный instruction set, когда дело доходит до conditional moves. В x86_64 есть
И в целом всё, есть только cmov. А вот в AArch64 аналогом является
oper может быть равно eq, ne и так далее. То есть csel if equal, csel if not equal и так далее.
Но помимо conditional select есть ещё многие другие, такие как conditional select increase/inverse/negate:
Но по мне самая недооцененная иструкция это conditional compare, дада, выглядит оно так
То есть можно манипулировать флагами сравнений после cmp через и таким образом проносить флаги дальше. Скажем, чтобы сравнить 16 байт между собой можно сделать
Такой трюк можно использовать, чтобы проносить результаты сравнений. Учитывая то, что ccmp занимает 1 цикл на Neoverse-n1 как и cmp, то это помогает сравнивать регионы памяти с тем же ptest/movmask на x86.
Вообще одно из самых сложных отличий SIMD x86 и ARM Neon заключается в movemask инструкции. Если коротко, для 16 байт оно забирает верхний бит каждого байта (если суффикс b) и обычно используется так
На 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, который выдавал некорректные результаты
Когда люди только в последнее десятилетие смотрели на 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, который выдавал некорректные результаты