RISC-V의 경우 아마도 GCC / clang을 사용하고있을 것입니다.
재미있는 사실 : GCC는 이러한 SWAR 비트 핵 트릭 중 일부를 알고 있으며 (다른 답변에 표시됨) 하드웨어 SIMD 명령이없는 대상에 대해 GNU C 기본 벡터 로 코드를 컴파일 할 때 사용할 수 있습니다 . 그러나 RISC-V의 clang은 순전히 스칼라 연산으로 롤을 풀므로 컴파일러에서 우수한 성능을 원한다면 직접 수행해야합니다.
기본 벡터 구문의 한 가지 장점은 하드웨어 SIMD가 있는 머신 을 대상으로 할 때 비트 핵이나 그와 같은 끔찍한 것을 자동 벡터화하는 대신이를 사용한다는 것입니다.
vector -= scalar
작업을 쉽게 작성할 수 있습니다. Just Works라는 구문은 암시 적으로 스칼라를 튀기는 일명 방송합니다.
또한 참고 uint64_t*
A로부터 부하가 uint8_t array[]
엄격한 앨리어싱 UB이다, 그래서 조심. (또한 glibc의 strlen이 왜 그렇게 빨리 실행되기 위해 그렇게 복잡해야 하는가?를 참고하십시오 . ISO C / C ++에서 작동 uint64_t
하는 char*
방식 과 같이 다른 객체에 액세스하기 위해 포인터 캐스트 할 수 있음 을 선언하기 위해 이와 같은 것을 원할 수 있습니다 .
다른 답변과 함께 사용하기 위해 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*
의 바이트에 액세스하는 것이 안전합니다 (그러나 uint8_t 배열의 경우는 반대 임). 좁은 요소 유형이이 특수한 경우에는 특별 하므로 엄격한 앨리어싱 문제를 피할 수 있습니다 .uint64_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.
}
HW SIMD가없는 RISC-V의 경우 vector_size(8)
효율적으로 사용할 수있는 입도 만 표현하고 작은 벡터의 두 배를 수행하는 데 사용할 수 있습니다.
그러나 vector_size(8)
GCC와 clang을 모두 사용하여 x86에 대해 매우 어리석게 컴파일합니다. GCC는 GP 정수 레지스터에서 SWAR 비트 핵을 사용하고 clang은 2 바이트 요소로 압축을 풀고 16 바이트 XMM 레지스터를 채우고 다시 압축합니다. (MMX는 너무 오래되어 GCC / clang은이를 사용하지 않아도됩니다. 적어도 x86-64에서는 그렇지 않습니다.)
그러나 vector_size (16)
( Godbolt )를 통해 우리는 기대 movdqa
/를 얻는다 paddb
. (에 의해 생성 된 올인원 벡터 포함 pcmpeqd same,same
). 함께 -march=skylake
우리는 여전히 하나 대신 YMM 두 개의 별도의 XMM 작전을 얻을, 그래서 불행하게도 현재의 컴파일러는 또한 넓은 벡터에없는 "자동 벡터화"벡터 작전을 수행 /
AArch64의 경우 vector_size(8)
( Godbolt ) 를 사용하는 것이 나쁘지 않습니다 . ARM / AArch64는 기본적으로 d
또는 q
레지스터 와 함께 8 바이트 또는 16 바이트 청크로 작동 할 수 있습니다 .
따라서 vector_size(16)
x86, RISC-V, ARM / AArch64 및 POWER에서 이식 가능한 성능을 원한다면 실제로 컴파일 하고 싶을 것입니다 . 그러나 일부 다른 ISA는 MIPS MSA와 같은 64 비트 정수 레지스터 내에서 SIMD를 수행합니다.
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 개의 명령으로 구성된 2 개의 체인이 XOR로 연결됩니다. @Reinstate Monica-ζ--의 답변은 4주기 뎁 체인 (x86 용)으로 컴파일됩니다. 5주기 루프 처리량은 sub
중요한 경로에 순진한 경로를 포함시켜 병목 현상이 발생하며 루프는 대기 시간에 병목 현상을 발생시킵니다.
그러나 이것은 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