Trừ các số nguyên 8 bit được đóng gói theo số nguyên 64 bit bằng 1 song song, SWAR không có SIMD phần cứng


77

Nếu tôi có một số nguyên 64 bit mà tôi đang hiểu là một mảng gồm các số nguyên 8 bit được đóng gói với 8 phần tử. Tôi cần phải trừ hằng số 1từ mỗi số nguyên được đóng gói trong khi xử lý tràn mà không có kết quả của một phần tử ảnh hưởng đến kết quả của phần tử khác.

Tôi có mã này vào lúc này và nó hoạt động nhưng tôi cần một giải pháp trừ đi từng số nguyên 8 bit được đóng gói song song và không truy cập bộ nhớ. Trên x86, tôi có thể sử dụng các hướng dẫn SIMD giống như psubbtrừ các số nguyên 8 bit được đóng gói song song nhưng nền tảng tôi đang mã hóa không hỗ trợ các hướng dẫn SIMD. (RISC-V trong trường hợp này).

Vì vậy, tôi đang cố gắng thực hiện SWAR (SIMD trong một thanh ghi) để hủy bỏ thủ công lan truyền giữa các byte của a uint64_t, thực hiện một thao tác tương đương với điều này:

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;
}

Tôi nghĩ bạn có thể làm điều này với các toán tử bitwise nhưng tôi không chắc chắn. Tôi đang tìm kiếm một giải pháp không sử dụng hướng dẫn SIMD. Tôi đang tìm kiếm một giải pháp trong C hoặc C ++ khá di động hoặc chỉ là lý thuyết đằng sau nó để tôi có thể thực hiện giải pháp của riêng mình.


5
Họ cần phải là 8 bit hay họ có thể là 7 bit?
tadman

Họ phải xin lỗi 8 bit :(
cam-trắng

12
Kỹ thuật cho loại điều này được gọi là SWAR
harold


1
Bạn có mong đợi một byte chứa 0 để bọc thành 0xff không?
Alnitak

Câu trả lời:


75

Nếu bạn có CPU với các hướng dẫn SIMD hiệu quả, SSE / MMX paddb( _mm_add_epi8) cũng khả thi. Câu trả lời của Peter Cordes cũng mô tả cú pháp vectơ GNU C (gcc / clang) và an toàn cho hàm răng cưa nghiêm ngặt. Tôi rất khuyến khích xem xét câu trả lời là tốt.

Tự làm với uint64_tnó là hoàn toàn di động, nhưng vẫn cần cẩn thận để tránh các vấn đề căn chỉnh và khử răng cưa nghiêm ngặt khi truy cập một uint8_tmảng với a uint64_t*. Bạn đã bỏ phần đó ra khỏi câu hỏi bằng cách bắt đầu với dữ liệu của bạn trong một uint64_t, nhưng đối với GNU C, một may_aliastypedef giải quyết vấn đề (xem câu trả lời của Peter cho điều đó hoặc memcpy).

Nếu không, bạn có thể phân bổ / khai báo dữ liệu của mình dưới dạng uint64_tvà truy cập thông qua uint8_t*khi bạn muốn từng byte riêng lẻ. unsigned char*được phép đặt bí danh bất cứ điều gì để khắc phục sự cố cho trường hợp cụ thể của các phần tử 8 bit. (Nếu uint8_ttồn tại ở tất cả, có thể an toàn khi cho rằng đó là một unsigned char.)


Lưu ý rằng đây là thay đổi từ thuật toán không chính xác trước đó (xem lịch sử sửa đổi).

Điều này là có thể mà không cần lặp cho phép trừ tùy ý và hiệu quả hơn cho một hằng số đã biết như 1trong mỗi byte. Thủ thuật chính là ngăn chặn thực hiện từ mỗi byte bằng cách đặt bit cao, sau đó sửa kết quả trừ.

Chúng tôi sẽ tối ưu hóa một chút kỹ thuật trừ được đưa ra ở đây . Họ định nghĩa:

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

với Hđịnh nghĩa là 0x8080808080808080U(tức là MSB của mỗi số nguyên đóng gói). Đối với một giảm, y0x0101010101010101U.

Chúng tôi biết rằng ytất cả các MSB của nó đều rõ ràng, vì vậy chúng tôi có thể bỏ qua một trong các bước mặt nạ (nghĩa y & ~Hlà giống như ytrong trường hợp của chúng tôi). Việc tính toán được tiến hành như sau:

  1. Chúng tôi đặt MSB của mỗi thành phần là x1, để khoản vay không thể truyền qua MSB sang thành phần tiếp theo. Gọi đây là đầu vào điều chỉnh.
  2. Chúng tôi trừ 1 từ mỗi thành phần, bằng cách trừ 0x01010101010101từ đầu vào đã sửa. Điều này không gây ra các khoản vay liên thành phần nhờ bước 1. Gọi đây là đầu ra được điều chỉnh.
  3. Bây giờ chúng ta cần sửa MSB của kết quả. Chúng tôi xor đầu ra được điều chỉnh với các MSB đảo ngược của đầu vào ban đầu để hoàn thành việc sửa kết quả.

Các hoạt động có thể được viết là:

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

Tốt hơn là, điều này được trình biên dịch nội tuyến (sử dụng các chỉ thị của trình biên dịch để ép buộc điều này) hoặc biểu thức được viết nội tuyến như là một phần của chức năng khác.

Testcase:

in:  0000000000000000
out: ffffffffffffffff

in:  f200000015000013
out: f1ffffff14ffff12

in:  0000000000000100
out: ffffffffffff00ff

in:  808080807f7f7f7f
out: 7f7f7f7f7e7e7e7e

in:  0101010101010101
out: 0000000000000000

Chi tiết hiệu suất

Đây là tập hợp x86_64 cho một lệnh gọi hàm duy nhất. Để có hiệu suất tốt hơn, cần phải phù hợp với hy vọng rằng các hằng số có thể sống trong một thanh ghi càng lâu càng tốt. Trong một vòng lặp chặt chẽ nơi các hằng số sống trong một thanh ghi, phần giảm thực tế có năm hướng dẫn: hoặc + không + và + thêm + xor sau khi tối ưu hóa. Tôi không thấy các lựa chọn thay thế sẽ đánh bại tối ưu hóa của nhà soạn nhạc.

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

Với một số thử nghiệm IACA của đoạn mã sau:

// 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;
}

chúng ta có thể chỉ ra rằng trên máy Skylake, thực hiện giảm, xor và so sánh + nhảy có thể được thực hiện chỉ dưới 5 chu kỳ trên mỗi lần lặp:

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  |
--------------------------------------------------------------------------------------------------

(Tất nhiên, trên x86-64, bạn chỉ cần tải hoặc movqvào một reg XMM paddb, vì vậy sẽ thú vị hơn khi xem cách nó biên dịch cho một ISA như RISC-V.)


4
Tôi cần mã của mình để chạy trên các máy RISC-V không có hướng dẫn SIMD (chưa), hãy để một mình hỗ trợ cho MMX
cam-trắng

2
@ cam-trắng Có nó - đây có lẽ là điều tốt nhất bạn có thể làm sau đó. Tôi cũng sẽ nhảy vào Godbolt để tỉnh táo kiểm tra lắp ráp cho RISC. Chỉnh sửa: Không hỗ trợ RISC-V trên godbolt :(
nanofarad

7
Thực sự có hỗ trợ RISC-V trên godbolt, ví dụ như thế này (E: dường như trình biên dịch trở nên quá sáng tạo trong việc tạo mặt nạ ..)
harold

4
Đọc thêm về cách sử dụng thủ thuật chẵn lẻ (còn gọi là "vectơ mang ra") trong các tình huống khác nhau: emulators.com/docs/LazyOverflowDetect_Final.pdf
jpa

4
Tôi đã thực hiện một chỉnh sửa khác; Các vectơ gốc GNU C thực sự tránh các vấn đề răng cưa nghiêm ngặt; một vectơ của uint8_tđược phép bí danh uint8_tdữ liệu. Những người gọi chức năng của bạn (cần lấy uint8_tdữ liệu vào a uint64_t) là những người phải lo lắng về vấn đề răng cưa nghiêm ngặt! Vì vậy, có lẽ OP chỉ nên khai báo / phân bổ các mảng uint64_tchar*được phép đặt bí danh bất cứ điều gì trong ISO C ++, nhưng không phải ngược lại.
Peter Cordes

16

Đố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 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 -= scalarhoạ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_trằ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à memcpyvào một uint64_t, cũng loại bỏ alignof(uint64_tyê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 memcpykhi 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.

uint8_tgần như chắc chắn unsigned char*, việc truy cập các byte của một uint64_tthô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ì charnó đặ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 intnhưng không floathay uint8_thoặ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=skylakechú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 dhoặc các qthanh 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ơ subtrê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

13

Tôi chỉ ra rằng mã bạn đã viết thực sự véc tơ một khi bạn bắt đầu xử lý nhiều hơn một uint64_t.

https://godbolt.org/z/J9DRzd


1
Bạn có thể giải thích hoặc đưa ra một tài liệu tham khảo cho những gì đang xảy ra ở đó? Có vẻ khá thú vị.
n314159

2
Tôi đã cố gắng làm điều này mà không có hướng dẫn SIMD nhưng tôi thấy điều này thú vị không kém :)
màu trắng

8
Mặt khác, mã SIMD đó là khủng khiếp. Trình biên dịch hoàn toàn hiểu sai những gì đang xảy ra ở đây. E: đó là một ví dụ về "điều này được trình biên dịch thực hiện rõ ràng bởi vì không có con người nào ngu ngốc như thế này"
harold

1
@PeterCordes: Tôi đã suy nghĩ nhiều hơn về các __vector_loop(index, start, past, pad)cấu trúc mà một triển khai có thể coi là for(index=start; index<past; index++)[có nghĩa là bất kỳ triển khai nào cũng có thể xử lý mã bằng cách sử dụng nó, chỉ bằng cách xác định một macro], nhưng sẽ có ngữ nghĩa lỏng lẻo hơn để mời một trình biên dịch xử lý mọi thứ trong bất kỳ kích thước chunk hai sức mạnh nào lên đến pad, kéo dài bắt đầu xuống và kết thúc lên nếu chúng chưa là bội số của kích thước chunk. Các tác dụng phụ trong mỗi khối sẽ không được xử lý và nếu breakxảy ra trong vòng lặp, các đại diện khác ...
supercat

1
@PeterCordes: Mặc dù restrictrất hữu ích (và sẽ hữu ích hơn nếu Tiêu chuẩn nhận ra khái niệm "ít nhất có khả năng dựa trên", và sau đó xác định "dựa trên" và "ít nhất có khả năng dựa trên" một cách thẳng thắn mà không gặp rắc rối và không thể thực hiện được đề xuất của tôi cũng sẽ cho phép một trình biên dịch thực hiện nhiều lần thực thi vòng lặp hơn yêu cầu - một cái gì đó sẽ đơn giản hóa rất nhiều vector hóa, nhưng mà Standard không đưa ra quy định nào.
supercat

11

Bạn có thể đảm bảo phép trừ không tràn và sau đó sửa bit cao:

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;
}

Tôi nghĩ rằng nó hoạt động cho tất cả 256 giá trị có thể của một byte; Tôi đặt nó trên Godbolt (với RISC-V clang ) godbolt.org/z/DGL9aq để xem kết quả lan truyền không đổi cho các đầu vào khác nhau như 0x0, 0x7f, 0x80 và 0xff (chuyển vào giữa số). Có vẻ tốt. Tôi nghĩ rằng câu trả lời hàng đầu sôi lên với cùng một điều, nhưng nó giải thích nó theo một cách phức tạp hơn.
Peter Cordes

Trình biên dịch có thể thực hiện công việc xây dựng các hằng số tốt hơn trong các thanh ghi ở đây. clang dành rất nhiều hướng dẫn xây dựng splat(0x01)splat(0x80), thay vì lấy cái này từ cái kia bằng ca. Ngay cả khi viết nó theo cách đó trong nguồn godbolt.org/z/6y9v-u không nắm tay trình biên dịch để tạo mã tốt hơn; nó chỉ truyền không đổi.
Peter Cordes

Tôi tự hỏi tại sao nó không tải hằng số từ bộ nhớ; đó là những gì trình biên dịch cho Alpha (một kiến ​​trúc tương tự) làm.
Falk Hüffner

GCC cho RISC-V không tải các hằng số từ bộ nhớ. Có vẻ như clang cần một số điều chỉnh, trừ khi dự kiến ​​bỏ lỡ bộ đệm dữ liệu và đắt tiền so với thông lượng lệnh. (Sự cân bằng đó chắc chắn có thể thay đổi kể từ Alpha và có lẽ các cách triển khai RISC-V khác nhau là khác nhau. Trình biên dịch cũng có thể làm tốt hơn nhiều nếu họ nhận ra rằng đó là mô hình lặp lại mà họ có thể thay đổi / HOẶC mở rộng sau khi bắt đầu với một LUI / thêm cho 20 + 12 = 32 bit dữ liệu ngay lập tức. Các mô hình bit của AArch64 thậm chí có thể sử dụng những dữ liệu này như là trực tiếp cho AND / OR / XOR, giải mã thông minh so với lựa chọn mật độ)
Peter Cordes

Đã thêm câu trả lời hiển thị SWAR vector gốc của GCC cho RISC-V
Peter Cordes

7

Không chắc đây có phải là điều bạn muốn không nhưng nó thực hiện 8 phép trừ song song với nhau:

#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;
}

Giải thích: Bitmask bắt đầu bằng 1 trong mỗi số 8 bit. Chúng tôi xor nó với đối số của chúng tôi. Nếu chúng ta có 1 ở nơi này, chúng ta đã trừ 1 và phải dừng lại. Điều này được thực hiện bằng cách đặt bit tương ứng thành 0 trong new_mask. Nếu chúng ta có 0, chúng ta đặt nó thành 1 và phải thực hiện việc mang theo, vì vậy bit giữ nguyên 1 và chúng ta dịch chuyển mặt nạ sang trái. Bạn nên tự kiểm tra xem liệu thế hệ của mặt nạ mới có hoạt động như dự định không, tôi nghĩ vậy, nhưng ý kiến ​​thứ hai sẽ không tệ.

PS: Tôi thực sự không chắc chắn nếu việc kiểm tra mask_cpkhông phải là null trong vòng lặp có thể làm chậm chương trình. Nếu không có nó, mã vẫn sẽ chính xác (vì mặt nạ 0 không làm gì cả) và trình biên dịch sẽ thực hiện việc hủy vòng lặp dễ dàng hơn nhiều.


forSẽ không chạy song song, bạn có nhầm lẫn với for_each?
LTPCGO

3
@LTPCGO Không, tôi không có ý định song song hóa điều này cho vòng lặp, điều này thực sự sẽ phá vỡ thuật toán. Nhưng mã này hoạt động song song trên các số nguyên 8 bit khác nhau trong số nguyên 64 bit, tức là tất cả 8 phép trừ được thực hiện đồng thời nhưng chúng cần tới 8 bước.
n314159

Tôi nhận ra những gì tôi đã hỏi có thể hơi vô lý nhưng điều này khá gần với những gì tôi cần cảm ơn :)
cam-trắng

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
} 

Bạn có thể làm điều đó với các thao tác bitwise bằng cách sử dụng ở trên và bạn chỉ cần chia số nguyên của mình thành các mảnh 8 bit để gửi 8 lần vào hàm này. Phần sau đây được lấy từ Cách chia số 64 bit thành tám giá trị 8 bit? với tôi thêm vào chức năng trên

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

Đó là C hoặc C ++ hợp lệ bất kể ai đó đi qua điều này như thế nào


5
Điều này không song song với công việc, đó là câu hỏi của OP.
nickelpro

Vâng @nickelpro đã đúng, điều này sẽ thực hiện lần lượt từng phép trừ, tôi muốn trừ tất cả các số nguyên 8 bit cùng một lúc. Tôi đánh giá cao câu trả lời tho cảm ơn bro
cam-trắng

2
@nickelpro khi tôi bắt đầu câu trả lời, bản chỉnh sửa đã không được thực hiện trong đó nêu phần song song của câu hỏi và vì vậy tôi đã không nhận thấy nó cho đến sau khi gửi, sẽ bỏ qua trong trường hợp nó hữu ích cho người khác vì ít nhất nó trả lời một phần để thực hiện các thao tác bitwise và nó có thể được thực hiện để hoạt động song song bằng cách sử dụng for_each(std::execution::par_unseq,...thay vì các khoảng trống
LTPCGO

2
Thật tệ, tôi đã gửi câu hỏi và sau đó nhận ra rằng tôi không cần phải song song chỉnh sửa như vậy
màu trắng

2

Không cố gắng đưa ra mã, nhưng để giảm 1, bạn có thể giảm theo nhóm 8 1 và sau đó kiểm tra để chắc chắn rằng các LSB của kết quả đã "lật". Bất kỳ LSB nào chưa được bật chỉ ra rằng việc thực hiện xảy ra từ 8 bit liền kề. Có thể tạo ra một chuỗi AND / OR / XOR để xử lý việc này mà không cần bất kỳ nhánh nào.


Điều đó có thể hoạt động, nhưng hãy xem xét trường hợp một carry mang lan truyền suốt một nhóm 8 bit và sang một nhóm khác. Chiến lược trong các câu trả lời tốt (đặt MSB hoặc một cái gì đó trước tiên) để đảm bảo mang theo không lan truyền có lẽ ít nhất là hiệu quả như điều này có thể. Mục tiêu hiện tại để đánh bại (tức là các câu trả lời không phân nhánh không vòng lặp tốt) là 5 lệnh RISC-V asm ALU với song song mức hướng dẫn làm cho đường dẫn quan trọng chỉ có 3 chu kỳ và sử dụng hai hằng 64 bit.
Peter Cordes

0

Tập trung làm việc trên từng byte hoàn toàn một mình, sau đó đặt nó trở lại vị trí cũ.

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;
   }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.