การลบจำนวนเต็ม 8 บิตในจำนวนเต็ม 64- บิตโดย 1 ในแบบขนาน, SWAR โดยไม่มีฮาร์ดแวร์ SIMD


77

ถ้าฉันมีจำนวนเต็ม 64- บิตที่ฉันตีความเป็นอาร์เรย์ของจำนวนเต็ม 8 บิตที่บรรจุด้วย 8 องค์ประกอบ ฉันต้องลบค่าคงที่1จากจำนวนเต็มที่บรรจุแต่ละตัวขณะจัดการล้นโดยไม่มีผลลัพธ์ขององค์ประกอบหนึ่งที่มีผลต่อผลลัพธ์ขององค์ประกอบอื่น

ฉันมีรหัสนี้ในขณะนี้และใช้งานได้ แต่ฉันต้องการวิธีแก้ปัญหาที่การลบของแต่ละจำนวนเต็ม 8 บิตบรรจุในแบบคู่ขนานและไม่ทำให้การเข้าถึงหน่วยความจำ ใน x86 ฉันสามารถใช้คำสั่ง SIMD เช่นpsubbนั้นลบจำนวนเต็ม 8 บิตพร้อมกัน แต่แพลตฟอร์มที่ฉันกำลังเข้ารหัสไม่รองรับคำแนะนำ 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 บิตขออภัย :(
cam-white

12
เทคนิคสำหรับสิ่งต่าง ๆ นี้เรียกว่าSWAR
harold


1
คุณคาดหวังไบต์มีศูนย์เพื่อห่อเพื่อ 0xff?
Alnitak

คำตอบ:


75

หากคุณมี CPU ที่มีคำสั่ง SIMD ที่มีประสิทธิภาพ SSE / MMX paddb( _mm_add_epi8) ก็สามารถใช้งานได้เช่นกัน คำตอบของ Peter Cordesยังอธิบายถึงไวยากรณ์ของเวกเตอร์ GNU C (gcc / clang) และความปลอดภัยสำหรับ UB ที่ใช้นามแฝงอย่างเข้มงวด ฉันขอแนะนำให้คุณทบทวนคำตอบนั้นด้วย

ทำมันด้วยตัวคุณเองกับuint64_tเป็นแบบพกพาอย่างเต็มที่ แต่ยังคงต้องมีการดูแลปัญหาการจัดตำแหน่งและหลีกเลี่ยงการเข้มงวด-aliasing UB เมื่อเข้าถึงอาร์เรย์ด้วยuint8_t uint64_t*คุณปล่อยให้คำถามนั้นหมดไปโดยเริ่มจากข้อมูลของคุณในuint64_tตอนนี้ แต่สำหรับ GNU C แล้วmay_aliastypedef แก้ปัญหาได้ (ดูคำตอบของปีเตอร์สำหรับสิ่งนั้นหรือ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(เช่น MSBs ของแต่ละจำนวนเต็ม) สำหรับการลดลงให้เป็นy0x0101010101010101U

เรารู้ว่าyMSB ทั้งหมดนั้นชัดเจนดังนั้นเราจึงสามารถข้ามหนึ่งในขั้นตอนมาสก์ได้ (เช่นy & ~Hเดียวกับyในกรณีของเรา) การคำนวณดำเนินการดังนี้:

  1. เราตั้งค่า MSB ของแต่ละองค์ประกอบxเป็น 1 เพื่อให้ผู้ยืมไม่สามารถเผยแพร่ MSB ที่ผ่านมาไปยังองค์ประกอบถัดไป เรียกสิ่งนี้ว่าอินพุตที่ปรับแล้ว
  2. เราลบ 1 จากแต่ละองค์ประกอบโดยการลบ0x01010101010101จากอินพุตที่แก้ไข สิ่งนี้ไม่ก่อให้เกิดการยืมระหว่างองค์ประกอบด้วยขั้นตอนที่ 1 เรียกสิ่งนี้ว่าเอาท์พุทที่ปรับแล้ว
  3. ตอนนี้เราต้องแก้ไข MSB ของผลลัพธ์ เราหรือปรับเอาท์พุทที่ปรับได้ด้วย MSB กลับด้านของอินพุตต้นฉบับเพื่อแก้ไขผลลัพธ์ให้เสร็จ

การดำเนินการสามารถเขียนเป็น:

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

โดยเฉพาะอย่างยิ่งสิ่งนี้ถูกคอมไพล์โดยคอมไพเลอร์ (ใช้คำสั่งคอมไพเลอร์เพื่อบังคับนี้) หรือนิพจน์จะถูกเขียนแบบอินไลน์เป็นส่วนหนึ่งของฟังก์ชั่นอื่น

Testcases:

in:  0000000000000000
out: ffffffffffffffff

in:  f200000015000013
out: f1ffffff14ffff12

in:  0000000000000100
out: ffffffffffff00ff

in:  808080807f7f7f7f
out: 7f7f7f7f7e7e7e7e

in:  0101010101010101
out: 0000000000000000

รายละเอียดประสิทธิภาพ

นี่คือชุดประกอบ x86_64 สำหรับการเรียกใช้ฟังก์ชันเดียว เพื่อประสิทธิภาพที่ดีขึ้นควรคาดหวังว่าค่าคงที่สามารถอยู่ในทะเบียนได้นานที่สุด ในการวนรอบอย่างแน่นหนาซึ่งค่าคงที่อยู่ในการลงทะเบียนการลดจริงจะใช้เวลาห้าคำสั่ง: หรือ + ไม่ + และ + เพิ่ม + 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 และเปรียบเทียบ + 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 สำหรับpaddbดังนั้นมันอาจน่าสนใจมากขึ้นที่จะดูว่ามันรวบรวม ISA เช่น RISC-V ได้อย่างไร)


4
ฉันต้องการรหัสของฉันเพื่อใช้กับเครื่อง RISC-V ซึ่งยังไม่มีคำสั่ง SIMD (ยัง) ให้การสนับสนุน MMX เพียงอย่างเดียวสำหรับ CamX
cam-white

2
@ cam-white เข้าใจแล้ว - นี่น่าจะดีที่สุดที่คุณสามารถทำได้ ฉันจะกระโดดขึ้นไปบน godbolt เพื่อสติตรวจสอบ RISC ด้วย แก้ไข: ไม่รองรับ RISC-V ใน godbolt :(
nanofarad

7
มีการสนับสนุน RISC-V บน godbolt เป็นจริงตัวอย่างเช่นเช่นนี้ (E: ดูเหมือนว่าคอมไพเลอร์ที่ได้รับการสร้างสรรค์สุดเหวี่ยงในการสร้างหน้ากาก .. )
แฮโรลด์

4
อ่านเพิ่มเติมเกี่ยวกับวิธีการที่เท่าเทียมกัน (เรียกว่า "เวกเตอร์พกพาออก") สามารถใช้ในสถานการณ์ต่าง ๆ : emulators.com/docs/LazyOverflowDetect_Final.pdf
jpa

4
ฉันแก้ไขอีกครั้ง เวกเตอร์ดั้งเดิมของ GNU C หลีกเลี่ยงปัญหาเรื่องนามแฝงที่เข้มงวด vector-of- uint8_tได้รับอนุญาตให้ใช้นามแฝงuint8_tข้อมูล ผู้ที่โทรเข้ามาฟังก์ชั่นของคุณ (ซึ่งจำเป็นต้องได้รับuint8_tข้อมูลเข้ามาuint64_t) เป็นสิ่งที่ต้องกังวลเกี่ยวกับนามแฝงที่เข้มงวด! ดังนั้น OP อาจประกาศ / จัดสรรอาร์เรย์uint64_tเนื่องจากchar*อนุญาตให้นามแฝงอะไรใน ISO C ++ แต่ไม่ใช่ในทางกลับกัน
Peter Cordes

16

สำหรับ RISC-V คุณอาจใช้ GCC / เสียงดังกราว

ความจริงแล้วสนุก: GCC รู้เทคนิค SWAR Bithack เหล่านี้บางส่วน (แสดงในคำตอบอื่น ๆ ) และสามารถใช้สำหรับคุณเมื่อรวบรวมรหัสกับเวกเตอร์ดั้งเดิมของ GNU Cสำหรับเป้าหมายโดยไม่มีคำแนะนำฮาร์ดแวร์ SIMD (แต่เสียงดังสำหรับ RISC-V จะเป็นการปลดการใช้งานแบบสเกลาร์ดังนั้นคุณต้องทำด้วยตัวเองถ้าคุณต้องการประสิทธิภาพที่ดีในคอมไพเลอร์)

ข้อดีอย่างหนึ่งของไวยากรณ์เวกเตอร์ดั้งเดิมคือเมื่อกำหนดเป้าหมายเครื่องด้วยฮาร์ดแวร์ SIMD จะใช้สิ่งนั้นแทนการปรับเวกเตอร์อัตโนมัติของคุณหรือสิ่งที่น่ากลัวอย่างนั้น

มันทำให้การเขียนง่ายvector -= scalarขึ้น ไวยากรณ์เพียงแค่ใช้งานโดยปริยายการกระจายสัญญาณหรือที่รู้จักก็คือการกระจายสเกลาร์ให้คุณ


โปรดทราบด้วยว่าการuint64_t*โหลดจาก a uint8_t array[]นั้นเป็น UB ที่ใช้นามแฝงอย่างเข้มงวดดังนั้นโปรดระมัดระวังด้วย (ดูเพิ่มเติมทำไม strlen ของ glibc จำเป็นต้องมีความซับซ้อนมากในการทำงานอย่างรวดเร็วหรือไม่ re: การทำให้ SWAR bithacks aliasing ที่เข้มงวดปลอดภัยใน C บริสุทธิ์) คุณอาจต้องการบางสิ่งเช่นนี้เพื่อประกาศuint64_tว่าคุณสามารถใช้พอยน์คาสต์เพื่อเข้าถึงออบเจ็กต์อื่น ๆ เช่นวิธีการchar*ทำงานใน ISO C / C ++

ใช้สิ่งเหล่านี้เพื่อรับข้อมูล 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อยู่ใน a uint64_tซึ่งจะลบalignof(uint64_tข้อกำหนดการจัดตำแหน่ง) ด้วย แต่สำหรับ ISAs ที่ไม่มีการโหลดที่ไม่ได้จัดแนวอย่างมีประสิทธิภาพ gcc / clang จะไม่อินไลน์และปรับให้เหมาะสมmemcpyเมื่อไม่สามารถพิสูจน์ได้ว่าตัวชี้นั้นอยู่ในแนวเดียวกันซึ่งจะส่งผลร้ายต่อประสิทธิภาพการทำงาน

TL: DR: ทางออกที่ดีที่สุดของคุณคือการประกาศข้อมูลเป็นuint64_t array[...]หรือจัดสรรแบบไดนามิกuint64_t, หรือโดยเฉพาะอย่างยิ่งalignas(16) uint64_t array[]; ที่ช่วยให้การจัดตำแหน่งอย่างน้อย 8 ไบต์หรือ 16 alignasหากคุณระบุ

เนื่องจากuint8_tเกือบจะแน่นอนunsigned char*จึงปลอดภัยในการเข้าถึงไบต์ของuint64_tผ่านuint8_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.
}

สำหรับ RISC-V ที่ไม่มี HW SIMD คุณสามารถใช้vector_size(8)เพื่อแสดงความละเอียดที่คุณสามารถใช้ได้อย่างมีประสิทธิภาพและทำเวกเตอร์ขนาดเล็กจำนวนสองเท่า

แต่vector_size(8)คอมไพล์อย่างน่าประหลาดใจสำหรับ x86 ด้วยทั้ง GCC และ clang: GCC ใช้ SWAR bithacks ในการลงทะเบียน GP-จำนวนเต็มเสียงดังกราว unpacks องค์ประกอบ 2 ไบต์เพื่อเติมลงทะเบียน XMM 16- ไบต์แล้ว repacks (MMX ล้าสมัยเหลือเกินที่ GCC / เสียงดังกังวานไม่ได้สนใจใช้มันอย่างน้อยก็สำหรับ x86-64)

แต่ด้วยvector_size (16)( Godbolt ) เราได้รับความคาดหวัง/movdqa paddb(ด้วยเวกเตอร์ทั้งหมดที่สร้างโดยpcmpeqd same,same) ด้วย-march=skylakeเรายังคงได้รับ XMM ops แยกกันสองตัวแทนที่จะเป็นหนึ่ง YMM ดังนั้นน่าเสียดายที่คอมไพเลอร์ปัจจุบันยังไม่ได้เวกเตอร์ "auto-vectorize" ใน ops ที่กว้างกว่า:

สำหรับ AArch64 มันไม่เลวเลยที่จะใช้vector_size(8)( Godbolt ) ARM / AArch64 สามารถทำงานในชิ้นส่วน 8 หรือ 16 ไบต์ได้ด้วยdหรือqรีจิสเตอร์

ดังนั้นคุณอาจต้องการvector_size(16)ที่จะรวบรวมจริงด้วยถ้าคุณต้องการประสิทธิภาพการทำงานแบบพกพาทั่ว x86, RISC-V, ARM / AArch64 และพลัง อย่างไรก็ตาม ISAs อื่น ๆ ทำ SIMD ภายในการลงทะเบียนจำนวนเต็ม 64 บิตเช่น MIPS MSA ฉันคิดว่า

vector_size(8)ทำให้ง่ายต่อการดู asm (เพียงหนึ่งค่าลงทะเบียนของข้อมูล): Godbolt compiler explorer

# 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 คำสั่งแต่ละอันนำไปสู่ ​​XOR @ Reinstate Monica - ζ - คำตอบของคอมไพล์กับ chain dep 4 รอบ (สำหรับ x86) ทรานแซคชันวนลูป 5 รอบได้รับการคอขวดโดยรวมถึงการไร้เดียงสาsubบนพา ธ วิกฤติ

อย่างไรก็ตามมันไม่มีประโยชน์อะไรกับเสียงดังกราว มันไม่ได้เพิ่มและจัดเก็บในลำดับเดียวกันกับที่โหลดดังนั้นมันจึงไม่ได้ทำการวางท่อซอฟต์แวร์ที่ดี!

# 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

ฉันสงสัยว่าทำไมมันไม่เพียงแค่โหลดค่าคงที่จากหน่วยความจำ นั่นคือสิ่งที่คอมไพเลอร์สำหรับ Alpha (สถาปัตยกรรมที่คล้ายคลึงกัน) ทำ
Falk Hüffner

GCC สำหรับ RISC-V ทำการโหลดค่าคงที่จากหน่วยความจำ ดูเหมือนว่าเสียงดังกราวต้องการการปรับแต่งบางอย่างเว้นแต่ว่าคาดว่าจะพลาดแคชข้อมูลและมีราคาแพงเมื่อเทียบกับปริมาณงานคำสั่ง (ความสมดุลนั้นสามารถเปลี่ยนแปลงได้อย่างแน่นอนตั้งแต่ Alpha และการใช้งานที่แตกต่างกันของ RISC-V นั้นแตกต่างกันผู้รวบรวมสามารถทำได้ดีกว่ามากหากพวกเขาตระหนักว่ามันเป็นรูปแบบการทำซ้ำที่พวกเขาสามารถเปลี่ยน / หรือขยายได้ . 20 + 12 = 32 บิตของข้อมูลทันที immediates บิตรูปแบบของ AArch64 ยังสามารถใช้เหล่านี้เป็น immediates สำหรับและ / หรือ / แฮคเกอร์, สมาร์ทถอดรหัสทางเลือกที่มีความหนาแน่นเทียบ)
ปีเตอร์ Cordes

เพิ่มคำตอบที่แสดง SWAR พื้นเมืองเวกเตอร์ของ GCC สำหรับ RISC-V
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;
}

คำอธิบาย: bitmask เริ่มต้นด้วย 1 ในแต่ละหมายเลข 8 บิต เรา xor กับอาร์กิวเมนต์ของเรา หากเรามี 1 ในสถานที่นี้เราจะลบ 1 และต้องหยุด สิ่งนี้ทำได้โดยการตั้งค่าบิตที่สอดคล้องกันเป็น 0 ใน new_mask ถ้าเรามี 0 เราตั้งมันไว้ที่ 1 และต้องพกติดตัวดังนั้นบิตก็อยู่ที่ 1 และเราเลื่อนหน้ากากไปทางซ้าย คุณควรตรวจสอบด้วยตัวเองว่าการสร้างหน้ากากใหม่นั้นทำงานได้ตามที่คาดไว้หรือไม่ แต่ความเห็นที่สองจะไม่เลวร้าย

PS: จริง ๆ แล้วฉันไม่แน่ใจว่าการตรวจสอบmask_cpว่าไม่เป็นโมฆะในลูปอาจทำให้โปรแกรมช้าลง หากไม่มีรหัสก็จะยังคงถูกต้อง (เนื่องจาก 0 mask ไม่ได้ทำอะไรเลย) และมันจะง่ายขึ้นมากสำหรับคอมไพเลอร์ในการทำการวนลูป


forจะไม่ทำงานในแบบคู่ขนานคุณสับสนfor_eachหรือไม่
LTPCGO

3
@ LTPCGO ไม่ฉันไม่ได้ตั้งใจจะขนานมันกับลูปนี่จะเป็นการทำลายอัลกอริธึม แต่รหัสนี้ใช้ได้กับเลขจำนวนเต็ม 8 บิตที่แตกต่างกันในจำนวนเต็ม 64 บิตแบบขนานนั่นคือการลบทั้งหมด 8 ครั้งจะทำพร้อมกัน แต่พวกเขาต้องการได้ถึง 8 ขั้นตอน
n314159

ผมทราบดีว่าสิ่งที่ฉันถูกถามอาจได้รับบิตไม่มีเหตุผล แต่ก็สวยใกล้เคียงกับสิ่งที่ฉันต้องการขอบคุณ :)
บแคมขาว

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 บิต กับฉันเพิ่มในฟังก์ชั่นด้านบน

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 บิตทั้งหมดในเวลาเดียวกัน ฉันขอขอบคุณคำตอบขอบคุณครับ bro
cam-white

2
@nickelpro เมื่อฉันเริ่มคำตอบการแก้ไขที่ไม่ได้ทำซึ่งระบุไว้เป็นส่วนคู่ของคำถามและดังนั้นฉันไม่ได้สังเกตจนกระทั่งหลังจากส่งจะทิ้งไว้ในกรณีที่เป็นประโยชน์สำหรับผู้อื่นอย่างน้อยตอบ มีส่วนร่วมในการดำเนินการระดับบิตและมันสามารถทำงานในแบบคู่ขนานโดยใช้for_each(std::execution::par_unseq,...แทน
whiles

2
มันเป็นสิ่งที่ไม่ดีของฉันฉันได้ส่งคำถามจากนั้นฉันก็รู้ว่าฉันไม่ได้บอกว่ามันจำเป็นต้องอยู่ในแบบคู่ขนานเพื่อแก้ไข
cam-white

2

จะไม่ลองรหัส แต่สำหรับการลดลง 1 คุณสามารถลดกลุ่ม 8 1s แล้วตรวจสอบเพื่อให้แน่ใจว่า LSB ของผลลัพธ์มี "พลิก" LSB ใด ๆ ที่ไม่ได้สลับแสดงว่ามีการพกพาเกิดขึ้นจาก 8 บิตที่อยู่ติดกัน มันควรจะเป็นไปได้ที่จะหาลำดับของ ANDs / ORs / XORs เพื่อจัดการเรื่องนี้โดยไม่มีสาขาใด ๆ


ที่อาจใช้งานได้ แต่ให้พิจารณากรณีที่ตัวดำเนินการแพร่กระจายตลอดทางผ่านกลุ่มหนึ่ง 8 บิตและเข้าไปอีกกลุ่มหนึ่ง กลยุทธ์ในคำตอบที่ดี (จากการตั้งค่า MSB หรือบางอย่างก่อน) เพื่อให้แน่ใจว่าการพกพาจะไม่เผยแพร่อย่างน้อยอาจมีประสิทธิภาพอย่างที่ควรจะเป็น เป้าหมายปัจจุบันที่จะเอาชนะ (เช่นคำตอบที่ไม่มีการวนซ้ำที่ดี) คือ 5 คำสั่ง RISC-V asm ALU พร้อมคำสั่งระดับคู่ขนานทำให้คำสั่งเส้นทางที่สำคัญเพียง 3 รอบและใช้ค่าคงที่ 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.