Experimental chill
9.2K subscribers
12 photos
1 video
2 files
109 links
Algorithms, libraries, C++, Linux, Distributed Systems, maybe Rust

Donate: https://t.me/experimentalchill/222

Author: @Danlark

Nothing in this blog is an opinion of my employer
Download Telegram
Channel created
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, который выдавал некорректные результаты
👍50🔥23