하드웨어 SIMD를 사용하지 않고 병렬로 1에서 병렬로 64 비트 정수에서 팩형 8 비트 정수 빼기 1 SWAR


77

64 비트 정수가있는 경우 8 개의 요소가있는 압축 된 8 비트 정수의 배열로 해석됩니다. 1한 요소의 결과가 다른 요소의 결과에 영향을 미치지 않고 오버플로를 처리하는 동안 각 팩형 정수에서 상수를 빼야합니다 .

나는이 코드를 가지고 있으며 작동하지만 각각의 8 비트 정수를 병렬로 빼고 메모리에 액세스하지 않는 솔루션이 필요합니다. x86에서는 psubb8 비트 정수를 병렬로 빼는 것과 같은 SIMD 명령을 사용할 수 있지만 코딩중인 플랫폼은 SIMD 명령을 지원하지 않습니다. (이 경우 RISC-V).

그래서 SWAR (레지스터 내의 SIMD) 을 수행하여 수동으로 a의 바이트 간 캐리 전파를 취소 uint64_t하려고합니다.

uint64_t sub(uint64_t arg) {
    uint8_t* packed = (uint8_t*) &arg;

    for (size_t i = 0; i < sizeof(uint64_t); ++i) {
        packed[i] -= 1;
    }

    return arg;
}

비트 연산자 로이 작업을 수행 할 수 있다고 생각하지만 확실하지 않습니다. SIMD 명령어를 사용하지 않는 솔루션을 찾고 있습니다. 나는 C 또는 C ++에서 상당히 이식성이 있거나 그 뒤에 이론이있는 솔루션을 찾고 있으므로 자체 솔루션을 구현할 수 있습니다.


5
8 비트 여야합니까, 아니면 7 비트 일 수 있습니까?
tadman

그들은 8 비트 죄송합니다 :(
캠 화이트

12
이런 종류의 물건에 대한 기술이라고 SWAR
해롤드


1
바이트에 0이 포함되어 0xff로 줄 바꿈됩니까?
Alnitak

답변:


75

효율적인 SIMD 명령어가있는 CPU가있는 경우 SSE / MMX paddb( _mm_add_epi8)도 실행 가능합니다. Peter Cordes의 답변 은 GNU C (gcc / clang) 벡터 구문과 엄격한 앨리어싱 UB의 안전성에 대해서도 설명합니다. 그 답변도 검토하는 것이 좋습니다.

직접 사용하는 uint64_t것은 이식성이 뛰어나지 만으로 uint8_t어레이에 액세스 할 때 정렬 문제와 엄격한 앨리어싱 UB를 피하기 위해주의를 기울여야 합니다 uint64_t*. uint64_t이미 데이터를 시작하여 그 부분을 질문 에서 제외했지만 GNU C의 경우 may_aliastypedef가 문제를 해결합니다 (Peter 's answer for the or memcpy).

그렇지 않으면 데이터를 할당 / 선언 하고 개별 바이트를 원할 때 uint64_t액세스 uint8_t*할 수 있습니다. unsigned char*는 8 비트 요소의 특정 경우에 대한 문제점을 회피하기 위해 무엇이든 별명을 지정할 수 있습니다. (만약 uint8_t존재 한다면 이라고 가정하는 것이 안전 할 것입니다 unsigned char.)


이것은 이전의 잘못된 알고리즘에서 변경된 것입니다 (수정 내역 참조).

이것은 임의의 빼기를 위해 루핑하지 않고 가능 1하며 각 바이트에서 와 같이 알려진 상수 에 대해 더 효율적입니다 . 주요 트릭은 높은 비트를 설정하여 각 바이트에서 캐리 아웃을 방지 한 다음 빼기 결과를 수정하는 것입니다.

우리는 여기에 주어진 빼기 기술을 약간 최적화 할 입니다. 그들은 다음을 정의합니다.

SWAR sub z = x - y
    z = ((x | H) - (y &~H)) ^ ((x ^~y) & H)

H정의 0x8080808080808080U(즉, 각 팩 정수의 MSB). 감소하려면 y입니다 0x0101010101010101U.

우리 y는 모든 MSB가 명확 하다는 것을 알고 있으므로 마스크 단계 중 하나를 건너 뛸 수 있습니다 (즉 , 우리의 경우 y & ~H와 동일 y). 계산은 다음과 같이 진행됩니다.

  1. x차용이 MSB를지나 다음 구성 요소로 전파 될 수 없도록 각 구성 요소의 MSB 를 1로 설정했습니다 . 이것을 조정 된 입력이라고합니다.
  2. 0x01010101010101수정 된 입력에서 빼서 각 구성 요소에서 1을 뺍니다 . 1 단계 덕분에 컴포넌트 간 차용이 발생하지 않습니다. 조정 된 출력을 호출하십시오.
  3. 이제 결과의 MSB를 수정해야합니다. 조정 된 출력을 원래 입력의 반전 된 MSB와 함께 조정하여 결과를 수정합니다.

작업은 다음과 같이 작성할 수 있습니다.

#define U64MASK 0x0101010101010101U
#define MSBON 0x8080808080808080U
uint64_t decEach(uint64_t i){
      return ((i | MSBON) - U64MASK) ^ ((i ^ MSBON) & MSBON);
}

바람직하게는 컴파일러에 의해 인라인되어 있거나 ( 컴파일러 지시문 을 사용 하여 강제 실행) 표현식이 다른 함수의 일부로 인라인으로 작성됩니다.

테스트 케이스 :

in:  0000000000000000
out: ffffffffffffffff

in:  f200000015000013
out: f1ffffff14ffff12

in:  0000000000000100
out: ffffffffffff00ff

in:  808080807f7f7f7f
out: 7f7f7f7f7e7e7e7e

in:  0101010101010101
out: 0000000000000000

성능 세부 사항

함수의 단일 호출을위한 x86_64 어셈블리는 다음과 같습니다. 더 나은 성능을 위해서는 상수가 가능한 한 레지스터에 존재할 수 있다는 희망과 함께 인라인되어야합니다. 상수가 레지스터에있는 타이트 루프에서 실제 감소에는 5 가지 명령이 필요합니다 : 최적화 후 or + not + and + add + xor. 컴파일러의 최적화를 능가하는 대안을 찾지 못했습니다.

uint64t[rax] decEach(rcx):
    movabs  rcx, -9187201950435737472
    mov     rdx, rdi
    or      rdx, rcx
    movabs  rax, -72340172838076673
    add     rax, rdx
    and     rdi, rcx
    xor     rdi, rcx
    xor     rax, rdi
    ret

다음 스 니펫에 대한 IACA 테스트

// Repeat the SWAR dec in a loop as a microbenchmark
uint64_t perftest(uint64_t dummyArg){
    uint64_t dummyCounter = 0;
    uint64_t i = 0x74656a6d27080100U; // another dummy value.
    while(i ^ dummyArg) {
        IACA_START
        uint64_t naive = i - U64MASK;
        i = naive + ((i ^ naive ^ U64MASK) & U64MASK);
        dummyCounter++;
    }
    IACA_END
    return dummyCounter;
}

Skylake 시스템에서 감소, xor 및 compare + jump를 수행 할 때마다 반복 당 5 사이클 미만으로 수행 할 수 있음을 보여줄 수 있습니다.

Throughput Analysis Report
--------------------------
Block Throughput: 4.96 Cycles       Throughput Bottleneck: Backend
Loop Count:  26
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
|  Port  |   0   -  DV   |   1   |   2   -  D    |   3   -  D    |   4   |   5   |   6   |   7   |
--------------------------------------------------------------------------------------------------
| Cycles |  1.5     0.0  |  1.5  |  0.0     0.0  |  0.0     0.0  |  0.0  |  1.5  |  1.5  |  0.0  |
--------------------------------------------------------------------------------------------------

(물론 x86-64에서는에 movq대한 XMM reg 만로드하거나 paddbRISC-V와 같은 ISA에 대해 컴파일하는 방법을 살펴 보는 것이 더 흥미로울 수 있습니다.)


4
나는 (아직) SIMD 명령을 가지고 있지 않는 RISC-V 시스템에서 실행 혼자 MMX를 지원하도록하려면 코드가 필요
캠 흰색을

2
@ cam-white 이해했습니다. 그러면 아마도 최선을 다할 것입니다. 나는 RISC의 어셈블리를 확인하기 위해 godbolt에 뛰어들 것이다. 편집 : :( godbolt 없음 RISC-V 지원
나노 패럿

7
실제로 godbolt에 대한 RISC-V 지원 이 있습니다. 예를 들면 다음과 같습니다 (E : 컴파일러가 마스크를 만들 때 지나치게 창의적으로 보입니다.)
harold

4
다양한 상황에서 패리티 ( "캐리어 아웃 벡터"라고도 함) 트릭을 사용하는 방법에 대한 추가 정보 : emulators.com/docs/LazyOverflowDetect_Final.pdf
jpa

4
나는 또 다른 편집을했다; GNU C 네이티브 벡터는 실제로 엄격한 앨리어싱 문제를 합니다. 벡터 uint8_tuint8_t데이터 별칭을 지정할 수 있습니다. 함수를 호출 하는 사람은 ( uint8_t데이터를 uint64_t에 가져와야 함) 엄격한 앨리어싱에 대해 걱정해야합니다! 따라서 OP는 ISO C ++에서 별칭을 지정할 수 uint64_t있기 때문에 배열을 선언 / 할당해야 char*하지만 그 반대의 경우도 마찬가지입니다.
Peter Cordes

16

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_tuint8_t*unsigned charchar


GNU C 네이티브 벡터 구문 예제 :

GNU C 고유 벡터는 항상 (자신의 기본 유형 별칭에 사용할 수 있습니다 예를 들어, int __attribute__((vector_size(16)))할 수있는 안전 별칭 int이 아닌 floatuint8_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

13

하나 이상의 uint64_t를 다루기 시작하면 작성한 코드가 실제로 벡터화된다는 것을 지적합니다.

https://godbolt.org/z/J9DRzd


1
거기에서 무슨 일이 일어나고 있는지 설명하거나 언급 할 수 있습니까? 꽤 흥미로운 것 같습니다.
n314159

2
나는 SIMD 명령 없이이 작업을 시도했지만이 흥미로운 것을 발견했습니다 :)
cam-white

8
반면 SIMD 코드는 끔찍합니다. 컴파일러는 여기서 무슨 일이 일어나고 있는지 완전히 이해하지 못했습니다. E : 이것은 "어리석은 사람이 아니기 때문에 컴파일러가 명확하게 수행 한 것"의 예입니다
해롤드

1
@ PeterCordes : __vector_loop(index, start, past, pad)구현이 처리 할 수 있는 구문을 더 많이 생각하고 있었지만 ( for(index=start; index<past; index++)어떤 구현이 매크로를 정의하여 코드를 사용하여 코드를 처리 할 수 ​​있음을 의미하지만) 컴파일러가 작업을 처리하도록 초대하는 의미가 느슨합니다. 청크 크기의 pad배수가 아닌 경우 시작점을 아래로 확장하고 위쪽으로 끝납니다. 각 청크 내의 부작용은 순서가 없으며 break, 루프 내에서 발생 하면 다른 담당자는 ...
supercat

1
@PeterCordes : restrict유용 하지만 표준이 "적어도 잠재적으로 기반"이라는 개념을 인식 한 다음 구피가없고 구석 구석이없는 "직접 기반"및 "적어도 잠재적 기반"으로 정의한 경우 더 유용합니다. 내 제안은 또한 컴파일러가 요청 된 것보다 더 많은 루프 실행을 수행하도록 허용합니다. 벡터화를 크게 단순화하지만 표준은 제공하지 않습니다.
supercat

11

빼기가 넘치지 않도록 한 다음 높은 비트를 수정하십시오.

uint64_t sub(uint64_t arg) {
    uint64_t x1 = arg | 0x80808080808080;
    uint64_t x2 = ~arg & 0x80808080808080;
    // or uint64_t x2 = arg ^ x1; to save one instruction if you don't have an andnot instruction
    return (x1 - 0x101010101010101) ^ x2;
}

256 바이트의 가능한 모든 값에 대해 작동한다고 생각합니다. Godbolt (RISC-V clang 포함) godbolt.org/z/DGL9aq에 배치 하여 0x0, 0x7f, 0x80 및 0xff (숫자 중간으로 이동)와 같은 다양한 입력에 대한 지속적인 전파 결과를 봅니다. 좋아 보인다 나는 가장 큰 대답이 같은 것으로 요약되지만 더 복잡한 방식으로 설명합니다.
Peter Cordes

컴파일러는 레지스터에서 상수를 구성하는 작업을 더 잘 수행 할 수 있습니다. 그 소리는 건설 지시를 많이 소비 splat(0x01)하고 splat(0x80)대신 변화와 함께 다른 하나를 얻는. 소스 godbolt.org/z/6y9v-u 에서 그런 식으로 쓰는 것조차도 컴파일러가 더 나은 코드를 만들기 위해 손을 쥐지 않습니다. 그것은 단지 지속적인 전파를합니다.
Peter Cordes

왜 메모리에서 상수를로드하지 않는지 궁금합니다. 이것이 알파 (유사한 아키텍처)의 컴파일러가하는 일입니다.
포크 Hüffner

RISC-V에 대한 GCC는 않습니다 메모리에서 부하 상수. 데이터 캐시 미스가 예상되지 않고 명령 처리량에 비해 비싸지 않으면 clang에 약간의 조정이 필요한 것 같습니다. (알파 이후 균형이 확실히 바뀌었을 가능성이 있으며 아마도 다른 RISC-V 구현은 다를 수 있습니다. 컴파일러는 반복되는 패턴임을 알면 훨씬 더 나은 결과를 얻을 수있었습니다. 20 + 12 = 32 비트의 즉각적인 데이터의 경우 AArch64의 비트 패턴 즉시는 이것을 AND / OR / XOR, 스마트 디코드와 밀도 선택에 대한 즉시로 사용할 수 있습니다)
Peter Cordes

RISC-V에 대한 GCC의 기본 벡터 SWAR을 보여주는 답변 을 추가 함
Peter Cordes

7

이것이 원하는지 확실하지 않지만 서로 8 개의 뺄셈을 수행합니다.

#include <cstdint>

constexpr uint64_t mask = 0x0101010101010101;

uint64_t sub(uint64_t arg) {
    uint64_t mask_cp = mask;
    for(auto i = 0; i < 8 && mask_cp; ++i) {
        uint64_t new_mask = (arg & mask_cp) ^ mask_cp;
        arg = arg ^ mask_cp;
        mask_cp = new_mask << 1;
    }
    return arg;
}

설명 : 비트 마스크는 각 8 비트 숫자에서 1로 시작합니다. 우리는 우리의 주장으로 그것을 조정합니다. 이 곳에 1이 있으면 1을 빼고 멈춰야합니다. new_mask에서 해당 비트를 0으로 설정하면됩니다. 만약 0이 있다면, 1로 설정하고 캐리를해야하므로 비트는 1을 유지하고 마스크를 왼쪽으로 이동시킵니다. 새 마스크의 생성이 의도 한대로 작동하는지 스스로 확인하는 것이 좋습니다. 그러나 제 2의 의견은 나쁘지 않습니다.

추신 : 실제로 mask_cp루프에서 null이 아닌 검사 가 프로그램 속도를 늦출 수 있는지 확실 하지 않습니다. 그것이 없으면 코드는 여전히 정확합니다 (0 마스크는 아무것도하지 않기 때문에) 컴파일러가 루프 언 롤링을 수행하는 것이 훨씬 쉽습니다.


for병렬로 실행되지 않습니다 for_each.
LTPCGO

3
@LTPCGO 아니요,이 for 루프를 병렬화하려는 의도는 아닙니다. 실제로 알고리즘이 중단됩니다. 그러나이 코드는 64 비트 정수의 서로 다른 8 비트 정수에서 병렬로 작동합니다. 즉, 8 개의 뺄셈이 동시에 수행되지만 최대 8 단계가 필요합니다.
n314159

나는 내가 요구했던 것이 약간 불합리했을 수도 있다는 것을 알고 있지만 이것은 내가 필요한 감사와 거의 비슷했다. :)
cam-white

4
int subtractone(int x) 
{
    int f = 1; 

    // Flip all the set bits until we find a 1 at position y
    while (!(x & f)) { 
        x = x^f; 
        f <<= 1; 
    } 

    return x^f; // return answer but remember to flip the 1 at y
} 

위의 내용을 사용하여 비트 단위 작업 으로이 작업을 수행 할 수 있으며 정수를 8 비트 조각으로 나누면이 함수에 8 번 보낼 수 있습니다. 다음 부분은 64 비트 숫자를 8 개의 8 비트 값으로 나누는 방법 에서 가져 왔습니다 . 위의 기능을 추가하면서

uint64_t v= _64bitVariable;
uint8_t i=0,parts[8]={0};
do parts[i++] = subtractone(v&0xFF); while (v>>=8);

누군가가 이것을 어떻게 접하는 지에 관계없이 유효한 C 또는 C ++입니다.


5
이것은 작업을 병렬화하지는 않지만 OP의 질문입니다.
nickelpro

네, @nickelpro가 맞습니다. 이것은 각 뺄셈을 차례로 수행 할 것입니다. 동시에 8 비트 정수를 모두 빼고 싶습니다. 나는 감사 형제 상점 답 감사드립니다
캠 흰색

2
@ nickelpro 내가 대답을 시작했을 때 편집이 이루어 지지 않았기 때문에 질문의 병렬 부분을 언급 했으므로 제출 후까지 그것을 알지 못했습니다. 비트 연산을 수행하고 for_each(std::execution::par_unseq,...
while

2
그것은 나쁘다, 나는 질문을 제출했다. 그리고 그것이 나란히 그렇게 편집되어야한다고 말하지 않았다는 것을 깨달았다
cam-white

2

코드를 만들려고 시도하지는 않지만 1 씩 줄이면 8 1 그룹으로 줄인 다음 결과의 LSB가 "flipped"되었는지 확인할 수 있습니다. 토글되지 않은 LSB는 인접한 8 비트에서 캐리가 발생했음을 나타냅니다. 분기없이이를 처리하기 위해 일련의 AND / OR / XOR을 처리 할 수 ​​있어야합니다.


그것은 효과가있을 수 있지만 캐리가 한 그룹의 8 비트를 통해 다른 그룹으로 전파되는 경우를 고려하십시오. 캐리가 전파되지 않도록하는 좋은 답변의 전략 (MSB 또는 무엇인가를 먼저 설정하는 것)은 아마도 최소한 효율적일 것입니다. 현재 달성 할 목표 (즉, 좋은 비 루핑 브랜치리스 응답)는 5 개의 RISC-V asm ALU 명령으로 명령 레벨 병렬 처리를 통해 중요한 경로를 3 주기만 만들고 2 개의 64 비트 상수를 사용합니다.
Peter Cordes

0

각 바이트에 대한 작업에 완전히 집중 한 다음 원래 위치로 되돌려 놓으십시오.

uint64_t sub(uint64_t arg) {
   uint64_t res = 0;

   for (int i = 0; i < 64; i+=8) 
     res += ((arg >> i) - 1 & 0xFFU) << i;

    return res;
   }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.