Para RISC-V probablemente estés usando GCC / clang.
Dato curioso: GCC conoce algunos de estos trucos de bithack SWAR (que se muestran en otras respuestas) y puede usarlos para usted cuando compila código con vectores nativos GNU C para objetivos sin instrucciones SIMD de hardware. (Pero el sonido metálico para RISC-V lo desenrollará ingenuamente a operaciones escalares, por lo que debe hacerlo usted mismo si desea un buen rendimiento en los compiladores).
Una ventaja de la sintaxis de vectores nativos es que cuando se dirige a una máquina con SIMD de hardware, la usará en lugar de vectorizar automáticamente su bithack o algo horrible como eso.
Facilita la escritura de vector -= scalar
operaciones; la sintaxis Just Works, transmitiendo implícitamente también conocido como salpicando el escalar por usted.
También tenga en cuenta que una uint64_t*
carga de un uint8_t array[]
UB es de alias estricto, así que tenga cuidado con eso. (Ver también ¿Por qué la strlen de glibc debe ser tan complicada para ejecutarse rápidamente? Re: hacer que los bithacks SWAR sean seguros con alias estricto en C puro). Es posible que desee algo como esto para declarar uint64_t
que puede lanzar puntero para acceder a cualquier otro objeto, como cómochar*
funciona en ISO C / C ++.
úselos para obtener datos de uint8_t en un uint64_t para usar con otras respuestas:
// 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)));
La otra forma de hacer cargas seguras de alias es con memcpy
a uint64_t
, que también elimina el alignof(uint64_t
requisito de alineación. Pero en los ISA sin cargas no alineadas eficientes, gcc / clang no memcpy
se alinea y optimiza cuando no pueden probar que el puntero está alineado, lo que sería desastroso para el rendimiento.
TL: DR: su mejor opción es declarar sus datos comouint64_t array[...]
o asignarlos dinámicamente como uint64_t
, o preferiblementealignas(16) uint64_t array[];
Eso asegura la alineación de al menos 8 bytes, o 16 si especificaalignas
.
Como uint8_t
es casi seguro unsigned char*
, es seguro acceder a los bytes de una uint64_t
vía uint8_t*
(pero no viceversa para una matriz uint8_t). Entonces, para este caso especial donde está el tipo de elemento estrecho unsigned char
, puede eludir el problema de alias estricto porque char
es especial.
Ejemplo de sintaxis de vector nativo de GNU C:
Vectores nativos GNU C siempre se permite a los alias con su tipo subyacente (por ejemplo, int __attribute__((vector_size(16)))
puede de manera segura alias int
pero no float
o uint8_t
o cualquier otra cosa.
#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.
}
Para RISC-V sin HW SIMD, puede usar vector_size(8)
para expresar solo la granularidad que puede usar de manera eficiente y hacer el doble de vectores más pequeños.
Pero vector_size(8)
compila muy estúpidamente para x86 tanto con GCC como con clang: GCC usa bithacks SWAR en registros GP-enteros, clang desempaqueta a elementos de 2 bytes para llenar un registro XMM de 16 bytes y luego los vuelve a empaquetar. (MMX es tan obsoleto que GCC / clang ni siquiera se molestan en usarlo, al menos no para x86-64).
Pero con vector_size (16)
( Godbolt ) obtenemos la espera movdqa
/ paddb
. (Con un vector de todos generados por pcmpeqd same,same
). Con -march=skylake
todavía obtenemos dos operaciones XMM separadas en lugar de una YMM, por lo que desafortunadamente los compiladores actuales tampoco "vectorizan automáticamente" las operaciones vectoriales en vectores más amplios: /
Para AArch64, no es tan malo usar vector_size(8)
( Godbolt ); ARM / AArch64 puede trabajar de forma nativa en fragmentos de 8 o 16 bytes con d
oq
registros.
Por lo tanto, es probable que desee vector_size(16)
compilar si desea un rendimiento portátil en x86, RISC-V, ARM / AArch64 y POWER . Sin embargo, algunos otros ISA hacen SIMD dentro de registros enteros de 64 bits, como MIPS MSA, creo.
vector_size(8)
hace que sea más fácil mirar el asm (solo un registro de datos): el explorador del compilador 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
Creo que es la misma idea básica que las otras respuestas sin bucle; evitando llevar y luego arreglando el resultado.
Estas son 5 instrucciones ALU, peor que la respuesta principal, creo. Pero parece que la latencia de ruta crítica es de solo 3 ciclos, con dos cadenas de 2 instrucciones cada una que conduce al XOR. @Reinstale las respuestas de Monica - ζ - a una cadena de dep de 4 ciclos (para x86). El rendimiento del ciclo de 5 ciclos tiene un cuello de botella al incluir también un ingenuosub
en la ruta crítica, y el ciclo tiene un cuello de botella en la latencia.
Sin embargo, esto es inútil con el sonido metálico. ¡Ni siquiera agrega y almacena en el mismo orden en que se cargó, por lo que ni siquiera está haciendo una buena canalización de software!
# 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