Đối với RISC-V, có lẽ bạn đang sử dụng GCC / clang.
Sự thật thú vị: GCC biết một số thủ thuật bithack SWAR này (được hiển thị trong các câu trả lời khác) và có thể sử dụng chúng cho bạn khi biên dịch mã với các vectơ gốc GNU C cho các mục tiêu mà không cần hướng dẫn SIMD phần cứng. (Nhưng tiếng kêu cho RISC-V sẽ ngây thơ hủy kết nối nó với các hoạt động vô hướng, vì vậy bạn phải tự làm điều đó nếu bạn muốn hiệu suất tốt trên các trình biên dịch).
Một lợi thế của cú pháp vectơ gốc là khi nhắm mục tiêu một máy có SIMD phần cứng, nó sẽ sử dụng nó thay vì tự động vectơ hóa bithack của bạn hoặc một cái gì đó kinh khủng như thế.
Nó làm cho nó dễ dàng để viết các vector -= scalar
hoạt động; cú pháp Chỉ hoạt động, phát sóng ngầm hay còn gọi là vô hướng cho bạn.
Cũng lưu ý rằng uint64_t*
tải từ một uint8_t array[]
UB có răng cưa nghiêm ngặt, vì vậy hãy cẩn thận với điều đó. (Xem thêm Tại sao strlen của glibc cần phải quá phức tạp để chạy nhanh? Re: làm cho bithacks SWAR an toàn nghiêm ngặt trong răng cưa thuần túy). Bạn có thể muốn một cái gì đó như thế này để khai báo uint64_t
rằng bạn có thể tạo con trỏ để truy cập vào bất kỳ đối tượng nào khác, như cách char*
hoạt động trong ISO C / C ++.
sử dụng chúng để nhận dữ liệu uint8_t vào uint64_t để sử dụng với các câu trả lời khác:
// 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)));
Một cách khác để thực hiện tải an toàn răng cưa là memcpy
vào một uint64_t
, cũng loại bỏ alignof(uint64_t
yêu cầu căn chỉnh). Nhưng trên ISA không có tải không được phân bổ hiệu quả, gcc / clang không nội tuyến và tối ưu hóa memcpy
khi chúng không thể chứng minh con trỏ được căn chỉnh, điều này sẽ là thảm họa cho hiệu suất.
TL: DR: đặt cược tốt nhất của bạn là khai báo dữ liệu của bạn dưới dạnguint64_t array[...]
hoặc phân bổ dữ liệu một cách linh hoạt uint64_t
, hoặc tốt nhấtalignas(16) uint64_t array[];
là đảm bảo căn chỉnh ít nhất 8 byte hoặc 16 nếu bạn chỉ định alignas
.
Vì uint8_t
gần như chắc chắn unsigned char*
, việc truy cập các byte của một uint64_t
thông qua uint8_t*
(nhưng không phải ngược lại đối với mảng uint8_t) là an toàn. Vì vậy, đối với trường hợp đặc biệt này có loại phần tử hẹp unsigned char
, bạn có thể bỏ qua vấn đề răng cưa nghiêm ngặt vì char
nó đặc biệt.
Ví dụ cú pháp vector gốc của GNU C:
Vectơ mẹ đẻ GNU C luôn được phép bí danh với loại tiềm ẩn của họ (ví dụ int __attribute__((vector_size(16)))
có thể bí danh một cách an toàn int
nhưng không float
hay uint8_t
hoặc bất cứ điều gì khác.
#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.
}
Đối với RISC-V mà không có bất kỳ SIM SIMD nào, bạn có thể sử dụng vector_size(8)
để thể hiện mức độ chi tiết mà bạn có thể sử dụng một cách hiệu quả và thực hiện gấp đôi số vectơ nhỏ hơn.
Nhưng vector_size(8)
biên dịch rất ngu ngốc cho x86 với cả GCC và clang: GCC sử dụng bithacks SWAR trong các thanh ghi số nguyên GP, clang giải nén thành phần tử 2 byte để điền vào thanh ghi XMM 16 byte sau đó đóng gói lại. (MMX quá lỗi thời đến nỗi GCC / clang thậm chí không bận tâm sử dụng nó, ít nhất là không dành cho x86-64.)
Nhưng với vector_size (16)
( Godbolt ), chúng ta có được sự mong đợi movdqa
/ paddb
. (Với một vectơ tất cả được tạo bởi pcmpeqd same,same
). Với -march=skylake
chúng tôi vẫn nhận được hai op XMM riêng thay vì một YMM, vì vậy thật không may, các trình biên dịch hiện tại cũng không "vectơ tự động" ops thành các vectơ rộng hơn: /
Đối với AArch64, nó không quá tệ để sử dụng vector_size(8)
( Godbolt ); ARM / AArch64 thực sự có thể hoạt động trong các khối 8 hoặc 16 byte với d
hoặc các q
thanh ghi.
Vì vậy, bạn có thể muốn vector_size(16)
thực sự biên dịch nếu bạn muốn hiệu năng di động trên x86, RISC-V, ARM / AArch64 và POWER . Tuy nhiên, một số ISA khác thực hiện SIMD trong các thanh ghi số nguyên 64 bit, như MIPS MSA tôi nghĩ.
vector_size(8)
làm cho nó dễ dàng hơn để xem asm (chỉ có một giá trị đăng ký dữ liệu): trình thám hiểm trình biên dịch 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
Tôi nghĩ đó là ý tưởng cơ bản giống như các câu trả lời không lặp khác; ngăn chặn thực hiện sau đó sửa chữa kết quả.
Đây là 5 hướng dẫn ALU, tệ hơn câu trả lời hàng đầu tôi nghĩ. Nhưng có vẻ như độ trễ đường dẫn quan trọng chỉ có 3 chu kỳ, với hai chuỗi gồm 2 hướng dẫn, mỗi chuỗi dẫn đến XOR. @Reinstate Monica - Câu trả lời của comp - biên dịch thành chuỗi dep 4 chu kỳ (cho x86). Thông lượng vòng lặp 5 chu kỳ bị tắc nghẽn bởi cũng bao gồm cả sự ngây thơ sub
trên đường dẫn quan trọng và vòng lặp không bị tắc nghẽn về độ trễ.
Tuy nhiên, điều này là vô ích với tiếng kêu. Nó thậm chí không thêm và lưu trữ theo cùng một thứ tự mà nó đã tải, do đó, nó thậm chí còn không làm tốt đường ống phần mềm!
# 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