Для RISC-V вы, вероятно, используете GCC / clang.
Интересный факт: GCC знает некоторые из этих хитростей SWAR-трюков (показанных в других ответах) и может использовать их для вас при компиляции кода с собственными векторами GNU C для целей без аппаратных инструкций SIMD. (Но clang для RISC-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
via 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.
}
Для RISC-V без какого-либо HW SIMD вы могли бы использовать vector_size(8)
для выражения только степень детализации, которую вы можете эффективно использовать, и сделать в два раза больше меньших векторов.
Но vector_size(8)
для x86 очень тупо компилируется как с GCC, так и clang: GCC использует битовые хаки 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, RISC-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
Я думаю, что это та же самая основная идея, что и другие нецикличные ответы; предотвращение переноса и исправление результата.
Это 5 инструкций ALU, хуже, чем главный ответ, я думаю. Но похоже, что задержка критического пути составляет всего 3 цикла, с двумя цепочками по 2 инструкции, каждая из которых ведет к XOR. Ответ @Reinstate Monica - ζ - компилируется в 4-тактную цепочку dep (для x86). Пропускная способность цикла с 5 циклами является узким местом, в том числе путем наивности sub
на критическом пути, а цикл создает узкое место с задержкой.
Тем не менее, это бесполезно с лязгом. Он даже не добавляет и не хранит в том же порядке, в котором загружен, поэтому он даже не выполняет хорошую программную конвейеризацию!
# 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