ฉันจะเขียน bit-mask ที่บำรุงรักษาได้รวดเร็วและคอมไพล์ไทม์ใน C ++ ได้อย่างไร


114

ฉันมีรหัสที่มากกว่าหรือน้อยกว่านี้:

#include <bitset>

enum Flags { A = 1, B = 2, C = 3, D = 5,
             E = 8, F = 13, G = 21, H,
             I, J, K, L, M, N, O };

void apply_known_mask(std::bitset<64> &bits) {
    const Flags important_bits[] = { B, D, E, H, K, M, L, O };
    std::remove_reference<decltype(bits)>::type mask{};
    for (const auto& bit : important_bits) {
        mask.set(bit);
    }

    bits &= mask;
}

เสียงดัง> = 3.6ทำสิ่งที่ชาญฉลาดและรวบรวมสิ่งนี้เป็นandคำสั่งเดียว(ซึ่งจะถูกแทรกในทุกที่):

apply_known_mask(std::bitset<64ul>&):  # @apply_known_mask(std::bitset<64ul>&)
        and     qword ptr [rdi], 775946532
        ret

แต่GCC ทุกเวอร์ชันที่ฉันพยายามรวบรวมสิ่งนี้มีความยุ่งเหยิงมหาศาลซึ่งรวมถึงการจัดการข้อผิดพลาดที่ควรเป็นแบบคงที่ DCE ในรหัสอื่น ๆ จะวางสิ่งที่important_bitsเทียบเท่าเป็นข้อมูลให้สอดคล้องกับรหัส!

.LC0:
        .string "bitset::set"
.LC1:
        .string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
        sub     rsp, 40
        xor     esi, esi
        mov     ecx, 2
        movabs  rax, 21474836482
        mov     QWORD PTR [rsp], rax
        mov     r8d, 1
        movabs  rax, 94489280520
        mov     QWORD PTR [rsp+8], rax
        movabs  rax, 115964117017
        mov     QWORD PTR [rsp+16], rax
        movabs  rax, 124554051610
        mov     QWORD PTR [rsp+24], rax
        mov     rax, rsp
        jmp     .L2
.L3:
        mov     edx, DWORD PTR [rax]
        mov     rcx, rdx
        cmp     edx, 63
        ja      .L7
.L2:
        mov     rdx, r8
        add     rax, 4
        sal     rdx, cl
        lea     rcx, [rsp+32]
        or      rsi, rdx
        cmp     rax, rcx
        jne     .L3
        and     QWORD PTR [rdi], rsi
        add     rsp, 40
        ret
.L7:
        mov     ecx, 64
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:.LC1
        xor     eax, eax
        call    std::__throw_out_of_range_fmt(char const*, ...)

ฉันจะเขียนโค้ดนี้อย่างไรเพื่อให้คอมไพเลอร์ทั้งสองสามารถทำสิ่งที่ถูกต้องได้ หากผิดพลาดฉันจะเขียนสิ่งนี้อย่างไรให้ชัดเจนรวดเร็วและบำรุงรักษาได้


4
แทนที่จะใช้ลูปคุณสร้างมาสก์ด้วยB | D | E | ... | Oไม่ได้หรือ?
HolyBlackCat

6
enum มีตำแหน่งบิตแทนที่จะขยายบิตอยู่แล้วดังนั้นฉันจึงทำได้(1ULL << B) | ... | (1ULL << O)
Alex Reinking

3
ข้อเสียคือชื่อจริงนั้นยาวและไม่สม่ำเสมอและไม่ง่ายเลยที่จะดูว่าแฟล็กใดอยู่ในมาสก์พร้อมกับเสียงรบกวนในบรรทัดทั้งหมด
Alex Reinking

4
@AlexReinking คุณสามารถทำให้เป็นหนึ่งได้(1ULL << Constant)| ต่อบรรทัดและจัดตำแหน่งชื่อคงที่ในบรรทัดต่างๆซึ่งจะง่ายกว่าในสายตา
einpoklum

ฉันคิดว่าปัญหาที่นี่เกี่ยวข้องกับการขาดการใช้งานประเภทที่ไม่ได้ลงนาม GCC มักจะมีปัญหากับการแก้ไขแบบคงที่สำหรับการโอเวอร์โฟลว์และการแปลงประเภทในไฮบริดที่ลงชื่อ / ไม่ได้ลงนามผลลัพธ์ของการเปลี่ยนบิตที่นี่เป็นintผลมาจากการดำเนินการบิตอาจเป็นintหรืออาจ long longขึ้นอยู่กับค่า และอย่างเป็นทางการenumไม่เทียบเท่ากับintค่าคงที่ เสียงดังเรียก "ราวกับ" gcc ยังคงอวดดี
Swift - Friday Pie

คำตอบ:


112

รุ่นที่ดีที่สุดคือ :

template< unsigned char... indexes >
constexpr unsigned long long mask(){
  return ((1ull<<indexes)|...|0ull);
}

แล้วก็

void apply_known_mask(std::bitset<64> &bits) {
  constexpr auto m = mask<B,D,E,H,K,M,L,O>();
  bits &= m;
}

ย้อนกลับไปใน เราสามารถทำเคล็ดลับแปลก ๆ นี้ได้:

template< unsigned char... indexes >
constexpr unsigned long long mask(){
  auto r = 0ull;
  using discard_t = int[]; // data never used
  // value never used:
  discard_t discard = {0,(void(
    r |= (1ull << indexes) // side effect, used
  ),0)...};
  (void)discard; // block unused var warnings
  return r;
}

หรือถ้าเราติดอยู่กับ เราสามารถแก้ซ้ำได้:

constexpr unsigned long long mask(){
  return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
  return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
  return mask(indexes...);
}

Godbolt กับทั้ง 3 - คุณสามารถสลับการกำหนด CPP_VERSION และรับการประกอบที่เหมือนกัน

ในทางปฏิบัติฉันใช้สิ่งที่ทันสมัยที่สุดเท่าที่จะทำได้ 14 beats 11 เพราะเราไม่มีการเรียกซ้ำดังนั้นความยาวสัญลักษณ์ O (n ^ 2) (ซึ่งสามารถระเบิดเวลาคอมไพล์และการใช้หน่วยความจำคอมไพเลอร์); 17 beats 14 เนื่องจากคอมไพเลอร์ไม่จำเป็นต้องตายรหัสกำจัดอาร์เรย์นั้นและเคล็ดลับอาร์เรย์นั้นน่าเกลียด

ในจำนวนนี้เป็นสิ่งที่สับสนที่สุด 14 ประการ ที่นี่เราสร้างอาร์เรย์ที่ไม่ระบุตัวตนของ 0 ทั้งหมดในขณะที่ผลข้างเคียงสร้างผลลัพธ์ของเราจากนั้นทิ้งอาร์เรย์ อาร์เรย์ที่ถูกทิ้งมีจำนวน 0s เท่ากับขนาดของแพ็คของเราบวก 1 (ซึ่งเราเพิ่มเพื่อให้เราสามารถจัดการกับแพ็คเปล่าได้)


คำอธิบายโดยละเอียดว่าไฟล์ เวอร์ชันกำลังทำอยู่ นี่เป็นเคล็ดลับ / แฮ็คและความจริงที่คุณต้องทำเพื่อขยายชุดพารามิเตอร์ด้วยประสิทธิภาพใน C ++ 14 เป็นสาเหตุหนึ่งที่ทำให้นิพจน์พับเมื่อเพิ่มเข้ามา.

เข้าใจได้ดีที่สุดจากภายในสู่ภายนอก:

    r |= (1ull << indexes) // side effect, used

นี้การปรับปรุงเพียงrกับ1<<indexesสำหรับดัชนีคงที่ indexesเป็นชุดพารามิเตอร์ดังนั้นเราจะต้องขยาย

ส่วนที่เหลือของงานคือการจัดเตรียมชุดพารามิเตอร์เพื่อขยายindexesภายใน

ขั้นตอนเดียว:

(void(
    r |= (1ull << indexes) // side effect, used
  ),0)

ที่นี่เราส่งนิพจน์ของเราไปvoidแสดงว่าเราไม่สนใจเกี่ยวกับค่าที่ส่งคืน (เราแค่ต้องการผลข้างเคียงของการตั้งค่าr- ใน C ++ นิพจน์เช่นจะa |= bคืนค่าที่ตั้งค่าaเป็น)

จากนั้นเราจะใช้ประกอบการจุลภาค,และ0ทิ้งvoid"ค่า" 0และส่งกลับค่า ดังนั้นนี่คือการแสดงออกที่มีค่า0และเป็นผลข้างเคียงของการคำนวณจะกำหนดบิตใน0r

  int discard[] = {0,(void(
    r |= (1ull << indexes) // side effect, used
  ),0)...};

indexesจุดนี้เราขยายแพ็คพารามิเตอร์ ดังนั้นเราจึงได้รับ:

 {
    0,
    (expression that sets a bit and returns 0),
    (expression that sets a bit and returns 0),
    [...]
    (expression that sets a bit and returns 0),
  }

ใน{}. การใช้,นี้ไม่ใช่ตัวดำเนินการลูกน้ำ แต่เป็นตัวคั่นองค์ประกอบอาร์เรย์ นี่คือsizeof...(indexes)+1 0s ซึ่งตั้งค่าบิตrเป็นผลข้างเคียง จากนั้นเราจะกำหนดคำแนะนำอาร์เรย์ก่อสร้างไปยังอาร์เรย์{}discard

ต่อไปเราจะส่งdiscardไปvoid- คอมไพเลอร์ส่วนใหญ่จะเตือนคุณหากคุณสร้างตัวแปรและไม่เคยอ่านเลย คอมไพเลอร์ทั้งหมดจะไม่บ่นถ้าคุณส่งไปvoidมันเป็นวิธีการที่จะพูดว่า "ใช่ฉันรู้ว่าฉันไม่ได้ใช้สิ่งนี้" ดังนั้นจึงระงับการเตือน


38
ขออภัยรหัส C ++ 14 นั้นเป็นอะไรบางอย่าง ไม่รู้เป็นอะไร
เจมส์

14
@ เจมส์เป็นตัวอย่างที่สร้างแรงบันดาลใจที่ยอดเยี่ยมว่าทำไมการแสดงออกถึงการพับใน C ++ 17 จึงน่ายินดี มันและเทคนิคที่คล้ายกันกลายเป็นวิธีที่มีประสิทธิภาพในการขยายแพ็ค "inplace" โดยไม่ต้องมีการเรียกซ้ำใด ๆ และซัพพลายเออร์พบว่าง่ายต่อการเพิ่มประสิทธิภาพ
Yakk - Adam Nevraumont

4
@ruben multi line constexpr ผิดกฎหมายในวันที่ 11
Yakk - Adam Nevraumont

6
ฉันไม่เห็นตัวเองกำลังเช็คอินรหัส C ++ 14 นั้น ฉันจะยึดติดกับ C ++ 11 หนึ่งเนื่องจากฉันต้องการสิ่งนั้น แต่ถึงแม้ว่าฉันจะใช้มันได้ แต่โค้ด C ++ 14 ก็ต้องการคำอธิบายมากมายที่ฉันจะไม่ทำ มาสก์เหล่านี้สามารถเขียนให้มีได้สูงสุด 32 องค์ประกอบดังนั้นฉันจึงไม่กังวลเกี่ยวกับพฤติกรรม O (n ^ 2) ท้ายที่สุดถ้า n ถูกล้อมรอบด้วยค่าคงที่มันก็คือ O (1) จริงๆ ;)
Alex Reinking

9
ให้กับผู้ที่พยายามที่จะเข้าใจ((1ull<<indexes)|...|0ull)ว่ามันเป็น"การแสดงออกเท่า" โดยเฉพาะมันคือ "binary right fold" และควรแยกวิเคราะห์เป็น(pack op ... op init)
Henrik Hansen

47

การเพิ่มประสิทธิภาพที่คุณกำลังมองหาดูเหมือนว่าจะมีการปอกเปลือกวงซึ่งมีการใช้งานที่หรือด้วยตนเอง-O3 -fpeel-loopsฉันไม่แน่ใจว่าเหตุใดสิ่งนี้จึงตกอยู่ภายใต้ขอบเขตของการลอกลูปแทนที่จะเป็นการคลายการวนซ้ำ แต่อาจเป็นไปได้ว่ามันไม่เต็มใจที่จะคลายลูปที่มีโฟลว์การควบคุมที่ไม่ใช่โลคัลอยู่ภายใน (ซึ่งอาจเป็นไปได้จากการตรวจสอบช่วง)

อย่างไรก็ตามตามค่าเริ่มต้น GCC จะหยุดไม่ให้สามารถลอกการทำซ้ำทั้งหมดซึ่งเห็นได้ชัดว่าจำเป็น จากการทดลองการส่งผ่าน-O2 -fpeel-loops --param max-peeled-insns=200(ค่าเริ่มต้นคือ 100) ทำให้งานเสร็จสิ้นด้วยรหัสเดิมของคุณ: https://godbolt.org/z/NNWrga


คุณยอดเยี่ยมมากขอบคุณ! ฉันไม่รู้ว่าสิ่งนี้สามารถกำหนดค่าได้ใน GCC! แม้ว่าจะ-O3 -fpeel-loops --param max-peeled-insns=200ล้มเหลวด้วยเหตุผลบางประการ... มันเป็นเพราะ-ftree-slp-vectorizeเห็นได้ชัด
Alex Reinking

โซลูชันนี้ดูเหมือนจะ จำกัด ไว้ที่เป้าหมาย x86-64 เอาต์พุตสำหรับ ARM และ ARM64 ยังไม่ค่อยสวยซึ่งอาจไม่เกี่ยวข้องกับ OP อย่างสมบูรณ์อีกครั้ง
เรียลไทม์

@realtime - มันค่อนข้างเกี่ยวข้องจริง ขอขอบคุณที่ชี้ให้เห็นว่ากรณีนี้ใช้ไม่ได้ น่าผิดหวังมากที่ GCC ตามไม่ทันก่อนที่จะถูกลดระดับเป็น IR เฉพาะแพลตฟอร์ม LLVM ปรับให้เหมาะสมก่อนที่จะลดระดับลงอีก
Alex Reinking

10

หากใช้เฉพาะ C ++ 11 เป็น(&a)[N]วิธีการจับอาร์เรย์ สิ่งนี้ช่วยให้คุณสามารถเขียนฟังก์ชันวนซ้ำหนึ่งฟังก์ชันโดยไม่ต้องใช้ฟังก์ชันตัวช่วยใด ๆ :

template <std::size_t N>
constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){
    return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull;
}

กำหนดให้กับconstexpr auto:

void apply_known_mask(std::bitset<64>& bits) {
    constexpr const Flags important_bits[] = { B, D, E, H, K, M, L, O };
    constexpr auto m = generate_mask(important_bits); //< here
    bits &= m;
}

ทดสอบ

int main() {
    std::bitset<64> b;
    b.flip();
    apply_known_mask(b);
    std::cout << b.to_string() << '\n';
}

เอาท์พุต

0000000000000000000000000000000000101110010000000000000100100100
//                                ^ ^^^  ^             ^  ^  ^
//                                O MLK  H             E  D  B

เราต้องขอบคุณความสามารถของ C ++ ในการคำนวณทุกอย่างที่คำนวณได้ในเวลาคอมไพล์ มันยังคงพัดใจฉันอยู่ ( <> )


สำหรับรุ่นที่ใหม่กว่า C ++ 14 และ C ++ 17 คำตอบของ yakkครอบคลุมสิ่งนั้นอย่างยอดเยี่ยมแล้ว


3
สิ่งนี้แสดงให้เห็นapply_known_maskได้อย่างไรว่าเพิ่มประสิทธิภาพได้จริง?
Alex Reinking

2
@AlexReinking: constexprทุกบิตที่น่ากลัวอยู่ และแม้ว่าในทางทฤษฎีจะไม่เพียงพอ แต่เรารู้ว่า GCC สามารถประเมินได้constexprตามที่ตั้งใจไว้
MSalters

8

ฉันขอแนะนำให้คุณเขียนEnumSetประเภทที่เหมาะสม

การเขียนพื้นฐานEnumSet<E>ใน C ++ 14 (เป็นต้นไป) โดยอาศัยพื้นฐานstd::uint64_tเป็นเรื่องเล็กน้อย:

template <typename E>
class EnumSet {
public:
    constexpr EnumSet() = default;

    constexpr EnumSet(std::initializer_list<E> values) {
        for (auto e : values) {
            set(e);
        }
    }

    constexpr bool has(E e) const { return mData & mask(e); }

    constexpr EnumSet& set(E e) { mData |= mask(e); return *this; }

    constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; }

    constexpr EnumSet& operator&=(const EnumSet& other) {
        mData &= other.mData;
        return *this;
    }

    constexpr EnumSet& operator|=(const EnumSet& other) {
        mData |= other.mData;
        return *this;
    }

private:
    static constexpr std::uint64_t mask(E e) {
        return std::uint64_t(1) << e;
    }

    std::uint64_t mData = 0;
};

สิ่งนี้ช่วยให้คุณสามารถเขียนโค้ดง่ายๆ:

void apply_known_mask(EnumSet<Flags>& flags) {
    static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };

    flags &= IMPORTANT;
}

ใน C ++ 11 ต้องใช้ Convolutions แต่ก็ยังคงเป็นไปได้:

template <typename E>
class EnumSet {
public:
    template <E... Values>
    static constexpr EnumSet make() {
        return EnumSet(make_impl(Values...));
    }

    constexpr EnumSet() = default;

    constexpr bool has(E e) const { return mData & mask(e); }

    void set(E e) { mData |= mask(e); }

    void unset(E e) { mData &= ~mask(e); }

    EnumSet& operator&=(const EnumSet& other) {
        mData &= other.mData;
        return *this;
    }

    EnumSet& operator|=(const EnumSet& other) {
        mData |= other.mData;
        return *this;
    }

private:
    static constexpr std::uint64_t mask(E e) {
        return std::uint64_t(1) << e;
    }

    static constexpr std::uint64_t make_impl() { return 0; }

    template <typename... Tail>
    static constexpr std::uint64_t make_impl(E head, Tail... tail) {
        return mask(head) | make_impl(tail...);
    }

    explicit constexpr EnumSet(std::uint64_t data): mData(data) {}

    std::uint64_t mData = 0;
};

และถูกเรียกใช้ด้วย:

void apply_known_mask(EnumSet<Flags>& flags) {
    static constexpr EnumSet<Flags> IMPORTANT =
        EnumSet<Flags>::make<B, D, E, H, K, M, L, O>();

    flags &= IMPORTANT;
}

แม้แต่ GCC ก็สร้างandคำสั่งที่-O1 godbolt :

apply_known_mask(EnumSet<Flags>&):
        and     QWORD PTR [rdi], 775946532
        ret

2
ในc ++ 11constexprโค้ดของคุณส่วนใหญ่ไม่ถูกกฎหมาย ฉันหมายความว่าบางคนมี 2 งบ! (C ++ 11 constexpr ดูด)
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont: คุณรู้หรือไม่ว่าฉันโพสต์รหัส2เวอร์ชันรุ่นแรกสำหรับ C ++ 14 เป็นต้นไปและรุ่นที่สองออกแบบมาเป็นพิเศษสำหรับ C ++ 11 หรือไม่? (เพื่ออธิบายข้อ จำกัด )
Matthieu M.

1
อาจจะดีกว่าถ้าใช้ std :: underlying_type แทน std :: uint64_t
เจมส์

@ เจมส์: จริงเปล่าครับ ทำทราบว่าEnumSet<E>ไม่ได้ใช้ค่าEเป็นค่าโดยตรง 1 << eแต่แทนที่จะใช้ มันเป็นโดเมนที่แตกต่างกันโดยสิ้นเชิงซึ่งจริง ๆ แล้วสิ่งที่ทำให้ชั้นเรียนที่มีคุณค่า => โอกาสของการตั้งใจทำดัชนีโดยไม่มีแทนe 1 << e
Matthieu M.

@MatthieuM ใช่คุณพูดถูก. ฉันสับสนกับการใช้งานของเราเองซึ่งคล้ายกับของคุณมาก ข้อเสียของการใช้ (1 << e) คือถ้า e อยู่นอกขอบเขตสำหรับขนาดของ underlying_type อาจเป็น UB หวังว่าจะเกิดข้อผิดพลาดของคอมไพเลอร์
เจมส์

7

ตั้งแต่ C ++ 11 คุณสามารถใช้เทคนิค TMP แบบคลาสสิกได้:

template<std::uint64_t Flag, std::uint64_t... Flags>
struct bitmask
{
    static constexpr std::uint64_t mask = 
        bitmask<Flag>::value | bitmask<Flags...>::value;
};

template<std::uint64_t Flag>
struct bitmask<Flag>
{
    static constexpr std::uint64_t value = (uint64_t)1 << Flag;
};

void apply_known_mask(std::bitset<64> &bits) 
{
    constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value;
    bits &= mask;
}

ลิงก์ไปยัง Compiler Explorer: https://godbolt.org/z/Gk6KX1

ข้อได้เปรียบของวิธีนี้เหนือฟังก์ชันเทมเพลต constexpr คืออาจรวบรวมได้เร็วกว่าเล็กน้อยเนื่องจากกฎของชิเอล


1

มีแนวคิดที่ 'ฉลาด' อยู่ที่นี่ คุณอาจไม่ได้ช่วยบำรุงรักษาโดยปฏิบัติตาม

คือ

{B, D, E, H, K, M, L, O};

เขียนง่ายกว่ามาก

(B| D| E| H| K| M| L| O);

?

จากนั้นไม่จำเป็นต้องใช้รหัสที่เหลือ


1
"B", "D" ฯลฯ ไม่ใช่แฟล็ก
MichałŁoś

ใช่คุณจะต้องแปลงค่าเหล่านี้เป็นแฟล็กก่อน นั่นไม่ชัดเจนในคำตอบของฉัน ขอโทษ ฉันจะอัปเดต
ANone
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.