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 -= scalaroperaciones; 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_tque 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 memcpya uint64_t, que también elimina el alignof(uint64_trequisito de alineación. Pero en los ISA sin cargas no alineadas eficientes, gcc / clang no memcpyse 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_tes casi seguro unsigned char*, es seguro acceder a los bytes de una uint64_tví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 chares 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 intpero no floato uint8_to 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=skylaketodaví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 doq 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