Для RISC-V ви, ймовірно, використовуєте GCC / clang.
Факт забави: GCC знає деякі з цих біт-трюків SWAR (показано в інших відповідях) і може використовувати їх для вас під час компіляції коду з націленими векторами GNU C для цілей без апаратних SIMD інструкцій. (Але клакс для RISC-V просто наївно розкрутить його до скалярних операцій, тому вам доведеться робити це самостійно, якщо ви хочете гарної продуктивності у компіляторів).
Однією з переваг нативного синтаксису векторів є те, що при націлюванні на машину з апаратним SIMD він використовуватиме те, що замість автоматичного векторизації вашого бітхака чи чогось жахливого подібного.
Це дозволяє легко писати vector -= scalar
операції; синтаксис «Працює», неявно транслюючи ака, як зарисує скаляр для вас.
Також зауважте, що uint64_t*
навантаження з АБ uint8_t array[]
є сувороїзмінним UB, тому будьте обережні з цим. (Див. Також, чому струн glibc повинен бути таким складним для швидкого запуску? Повторне: створення SWAR bithacks суворо-псевдонім безпечним у чистому С). Можливо, ви хочете, щоб щось подібне оголосило, 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 без будь-якої SIM-карти HW ви можете використовувати vector_size(8)
для вираження лише деталізацію, яку ви можете ефективно використовувати, і робити вдвічі більше дрібних векторів.
Але vector_size(8)
компілюється дуже нерозумно для x86 як з GCC, так і з клангом: GCC використовує біт-хаки SWAR в регістри GP-цілих чисел, кланг розпаковується до 2-байтових елементів, щоб заповнити 16-байтовий реєстр XMM, а потім перепакується. (MMX настільки застарілий, що GCC / clang навіть не заважає використовувати його, принаймні, не для x86-64.)
Але з vector_size (16)
( Godbolt ) ми отримуємо очікуване movdqa
/ paddb
. (З вектором, що генерується всіма pcmpeqd same,same
). Оскільки -march=skylake
ми все ще отримуємо два окремих XMM ops замість одного YMM, тому, на жаль, поточні компілятори також не "автоматизують" векторні ops у більш широкі вектори: /
Для 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