Для RIS C -V вы, вероятно, используете GCC / clang.
Интересный факт: G CC знает некоторые из этих хитростей SWAR-трюков (показанных в других ответах) и может использовать их для Вы при компиляции кода с GNU C нативными векторами для целей без аппаратных инструкций SIMD. (Но clang для RIS C -V просто наивно развернет его для скалярных операций, поэтому вам придется делать это самостоятельно, если вы хотите добиться хорошей производительности на всех компиляторах).
Одним из преимуществ нативного векторного синтаксиса является то, что при нацеливании на машину с аппаратным SIMD, он будет использовать ее вместо автоматической векторизации вашего битхака или чего-то ужасного в этом роде.
Это облегчает написание vector -= scalar
операций; синтаксис Just Works, неявно вещающий, иначе говоря, скаляр от вас.
Также обратите внимание, что загрузка uint64_t*
из uint8_t array[]
является строгим псевдонимом UB, поэтому будьте осторожны с этим. (См. Также Почему strlen glibc должен быть настолько сложным, чтобы быстро запускаться? re: сделать бит-хэки SWAR строгим и безопасным в чистом C). Возможно, вы захотите, чтобы что-то вроде этого объявило uint64_t
, что вы можете привести указатель для доступа к любым другим объектам, например, как char*
работает в ISO C / C ++.
использовать их для получить данные uint8_t в uint64_t для использования с другими ответами:
// GNU C: gcc/clang/ICC but not MSVC
typedef uint64_t aliasing_u64 __attribute__((may_alias)); // still requires alignment
typedef uint64_t aliasing_unaligned_u64 __attribute__((may_alias, aligned(1)));
Другой способ сделать безопасные для алиасов нагрузки - это memcpy
в uint64_t
, который также удаляет alignof(uint64_t
) требование выравнивания. Но на ISA без эффективных невыровненных нагрузок gcc / clang не встроен и не оптимизирует memcpy
, когда они не могут доказать, что указатель выровнен, что было бы катастрофично для производительности.
TL: DR: лучше всего объявить ваши данные как uint64_t array[...]
или динамически распределить их как uint64_t
, или предпочтительно alignas(16) uint64_t array[];
, что обеспечивает выравнивание по крайней мере до 8 байтов, или 16, если вы укажите alignas
.
Поскольку uint8_t
почти наверняка unsigned char*
, доступ к байтам uint64_t
через uint8_t*
безопасен (но не наоборот для массива uint8_t). Так что для этого особого случая, когда узкий тип элемента равен unsigned char
, вы можете обойти проблему строгого наложения имен, поскольку char
является особенным.
GNU C Пример синтаксиса собственного вектора:
GNU C родным векторам всегда разрешено создавать псевдонимы с их базовым типом (например, int __attribute__((vector_size(16)))
может безопасно использовать псевдоним int
, но не float
или uint8_t
или что-либо еще.
#include <stdint.h>
#include <stddef.h>
// assumes array is 16-byte aligned
void dec_mem_gnu(uint8_t *array) {
typedef uint8_t v16u8 __attribute__ ((vector_size (16), may_alias));
v16u8 *vecs = (v16u8*) array;
vecs[0] -= 1;
vecs[1] -= 1; // can be done in a loop.
}
Для RIS C -V без HW SIMD вы можете использовать vector_size(8)
до express только ту гранулярность, которую вы можете эффективно использовать, и использовать в два раза больше меньших векторов.
Но vector_size(8)
очень тупо компилируется для x86 как с G CC, так и clang: G CC использует битовые хаки SWAR в целочисленных регистрах GP, clang распаковывает в 2-байтовые элементы для заполнения 16-байтового регистра XMM, а затем перепаковывает. (MMX так устарел что GCC / clang даже не потрудился использовать его, по крайней мере, для x86-64.)
Но с vector_size (16)
( Godbolt ) мы получаем ожидаемое movdqa
/ paddb
. (С вектором из всех единиц, сгенерированным pcmpeqd same,same
). С -march=skylake
мы все еще получаем две отдельные операции XMM вместо одной YMM, так что, к сожалению, современные компиляторы также не "автоматически векторизуют" векторные операции в более широкие векторы: /
Для AArch64 не так уж плохо использовать vector_size(8)
( Godbolt ); ARM / AArch64 может изначально работать в 8- или 16-байтовых чанках с d
или q
регистрами.
Таким образом, вы, вероятно, захотите, чтобы vector_size(16)
действительно компилировался, если вы хотите переносить производительность на x86 , RIS C -V, ARM / AArch64 и POWER . Однако некоторые другие ISA выполняют SIMD в 64-битных целочисленных регистрах, например, MIPS MSA.
vector_size(8)
облегчает просмотр asm (только один регистр данных): Godbolt проводник компилятора
# GCC8.2 -O3 for RISC-V for vector_size(8) and only one vector
dec_mem_gnu(unsigned char*):
lui a4,%hi(.LC1) # generate address for static constants.
ld a5,0(a0) # a5 = load from function arg
ld a3,%lo(.LC1)(a4) # a3 = 0x7F7F7F7F7F7F7F7F
lui a2,%hi(.LC0)
ld a2,%lo(.LC0)(a2) # a2 = 0x8080808080808080
# above here can be hoisted out of loops
not a4,a5 # nx = ~x
and a5,a5,a3 # x &= 0x7f... clear high bit
and a4,a4,a2 # nx = (~x) & 0x80... inverse high bit isolated
add a5,a5,a3 # x += 0x7f... (128-1)
xor a5,a4,a5 # x ^= nx restore high bit or something.
sd a5,0(a0) # store the result
ret
Я думаю, что это та же самая базовая идея c, что и у других нецикличных ответов; предотвращение переноса, затем исправление результата.
Это 5 инструкций ALU, хуже, чем лучший ответ, я думаю. Но похоже, что задержка критического пути составляет всего 3 цикла, с двумя цепочками по 2 инструкции, каждая из которых ведет к XOR. Ответ @Reinstate Monica - ζ - компилируется в 4-тактную цепочку dep (для x86). Пропускная способность l oop в 5 циклах также является узким местом, поскольку на критическом пути также включается наивное sub
, а l oop создает узкие места при задержке.
Однако это бесполезно для clang. Он даже не добавляет и не хранит в том же порядке, в котором загружен, поэтому он даже не выполняет хорошую программную конвейеризацию!
# RISC-V clang (trunk) -O3
dec_mem_gnu(unsigned char*):
lb a6, 7(a0)
lb a7, 6(a0)
lb t0, 5(a0)
...
addi t1, a5, -1
addi t2, a1, -1
addi t3, a2, -1
...
sb a2, 7(a0)
sb a1, 6(a0)
sb a5, 5(a0)
...
ret