Untuk RISC-V Anda mungkin menggunakan GCC / dentang.
Fakta menyenangkan: GCC mengetahui beberapa trik bithack SWAR ini (diperlihatkan dalam jawaban lain) dan dapat menggunakannya untuk Anda ketika menyusun kode dengan vektor asli GNU C untuk target tanpa instruksi SIMD perangkat keras. (Tapi dentang untuk RISC-V akan secara naif membuka gulungannya ke operasi skalar, jadi Anda harus melakukannya sendiri jika Anda ingin kinerja yang baik di seluruh kompiler).
Salah satu keuntungan sintaksis vektor asli adalah bahwa ketika menargetkan mesin dengan SIMD perangkat keras, ia akan menggunakannya daripada secara otomatis membuat vektor bithack Anda atau sesuatu yang mengerikan seperti itu.
Ini membuatnya mudah untuk menulis vector -= scalar
operasi; sintaks Just Works, menyiarkan secara implisit alias membentangkan skalar untuk Anda.
Perhatikan juga bahwa uint64_t*
beban dari uint8_t array[]
UB adalah aliasing yang ketat, jadi berhati-hatilah dengan itu. (Lihat juga Mengapa strib glibc perlu sangat rumit untuk berjalan cepat? Re: membuat SWAR bithacks ketat-alias aman dalam C murni). Anda mungkin ingin sesuatu seperti ini mendeklarasikan uint64_t
bahwa Anda dapat mengarahkan penunjuk untuk mengakses objek lain, seperti caranyachar*
kerjanya di ISO C / C ++.
gunakan ini untuk mendapatkan data uint8_t menjadi uint64_t untuk digunakan dengan jawaban lain:
// 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)));
Cara lain untuk melakukan aliasing-safe load adalah dengan memcpy
menjadi uint64_t
, yang juga menghilangkan alignof(uint64_t
) persyaratan perataan. Tetapi pada ISA tanpa beban yang tidak selaras efisien, gcc / dentang tidak sejajar dan dioptimalkan memcpy
ketika mereka tidak dapat membuktikan bahwa pointer selaras, yang akan menjadi bencana bagi kinerja.
TL: DR: taruhan terbaik Anda adalah untuk mendeklarasikan data Anda sebagaiuint64_t array[...]
atau mengalokasikannya secara dinamis uint64_t
, atau lebih disukaialignas(16) uint64_t array[];
Itu memastikan keselarasan ke setidaknya 8 byte, atau 16 jika Anda menentukanalignas
.
Karena uint8_t
hampir pasti unsigned char*
, aman untuk mengakses byte uint64_t
via uint8_t*
(tetapi tidak sebaliknya untuk array uint8_t). Jadi untuk kasus khusus di mana jenis elemen sempit ini unsigned char
, Anda dapat menghindari masalah aliasing ketat karena char
khusus.
Contoh sintaks vektor asli GNU C:
GNU C vektor asli selalu diizinkan untuk alias dengan jenis yang mendasarinya (misalnya int __attribute__((vector_size(16)))
bisa dengan aman alias int
tetapi tidak float
atau uint8_t
atau apa pun.
#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.
}
Untuk RISC-V tanpa SIM HW, Anda dapat menggunakannya vector_size(8)
untuk mengekspresikan granularity yang dapat Anda gunakan secara efisien, dan melakukan dua kali lebih banyak vektor yang lebih kecil.
Tetapi vector_size(8)
mengkompilasi dengan sangat bodoh untuk x86 dengan GCC dan dentang: GCC menggunakan bithacks SWAR dalam register GP-integer, dentang membongkar ke elemen 2-byte untuk mengisi register XMM 16-byte kemudian mengemasnya kembali. (MMX sangat usang sehingga GCC / dentang bahkan tidak repot menggunakannya, setidaknya tidak untuk x86-64.)
Tetapi dengan vector_size (16)
( Godbolt ) kita mendapatkan yang diharapkan movdqa
/ paddb
. (Dengan semua vektor yang dihasilkan oleh pcmpeqd same,same
). Dengan-march=skylake
kita masih mendapatkan dua ops XMM terpisah dan bukan satu YMM, jadi sayangnya kompiler saat ini juga tidak "auto-vectorize" ops vektor ke dalam vektor yang lebih luas: /
Untuk AArch64, itu tidak terlalu buruk untuk digunakan vector_size(8)
( Godbolt ); ARM / AArch64 asli dapat bekerja dalam potongan 8 atau 16 byte dengan d
atauq
register.
Jadi Anda mungkin ingin vector_size(16)
mengompilasi jika Anda ingin kinerja portabel di x86, RISC-V, ARM / AArch64, dan POWER . Namun, beberapa SPA lain melakukan SIMD dalam register integer 64-bit, seperti MIPS MSA.
vector_size(8)
membuatnya lebih mudah untuk melihat asm (nilai data hanya satu register): Godbolt compiler explorer
# 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
Saya pikir itu ide dasar yang sama dengan jawaban non-looping lainnya; mencegah carry kemudian memperbaiki hasilnya.
Ini adalah 5 instruksi ALU, lebih buruk daripada jawaban atas yang saya pikir. Tapi sepertinya latensi jalur kritis hanya 3 siklus, dengan dua rantai 2 instruksi masing-masing mengarah ke XOR. @Reinstate Monica - jawaban comp - mengkompilasi ke rantai dep 4 siklus (untuk x86). Throughput loop 5 siklus dihambat oleh juga termasuk naifsub
di jalur kritis, dan loop tidak bottleneck pada latensi.
Namun, ini tidak berguna dengan dentang. Ia bahkan tidak menambah dan menyimpan dalam urutan yang sama saat dimuat sehingga bahkan tidak melakukan pipelining perangkat lunak yang baik!
# 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