Làm cách nào để viết một bit-mask có thể bảo trì, nhanh chóng, thời gian biên dịch trong C ++?


113

Tôi có một số mã giống như sau:

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

Clang> = 3.6 thực hiện điều thông minh và biên dịch nó thành một andlệnh duy nhất (sau đó sẽ được nội tuyến ở mọi nơi khác):

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

Nhưng mọi phiên bản GCC tôi đã thử đều biên dịch điều này thành một mớ hỗn độn khổng lồ bao gồm xử lý lỗi mà lẽ ra phải là DCE'd tĩnh. Trong mã khác, nó thậm chí sẽ đặt important_bitsdữ liệu tương đương theo dòng với mã!

.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*, ...)

Tôi nên viết mã này như thế nào để cả hai trình biên dịch đều có thể làm đúng? Không đạt được điều đó, tôi nên viết nó như thế nào để nó vẫn rõ ràng, nhanh chóng và có thể bảo trì được?


4
Thay vì sử dụng vòng lặp, bạn không thể tạo mặt nạ bằng B | D | E | ... | O?
HolyBlackCat

6
Enum có vị trí bit chứ không phải là bit đã được mở rộng, vì vậy tôi có thể làm(1ULL << B) | ... | (1ULL << O)
Alex Reinking

3
Nhược điểm là các tên thực tế dài và không đều và gần như không dễ dàng để xem lá cờ nào nằm trong mặt nạ với tất cả tiếng ồn dòng đó.
Alex Reinking

4
@AlexReinking Bạn có thể biến nó thành một (1ULL << Constant)| trên mỗi dòng và căn chỉnh các tên không đổi trên các dòng khác nhau, điều đó sẽ dễ nhìn hơn.
einpoklum

Tôi nghĩ vấn đề ở đây liên quan đến thiếu sử dụng loại unsigned, GCC luôn có rắc rối với tĩnh loại bỏ hiệu chỉnh cho tràn và loại chuyển đổi trong ký / hybrid.Result unsigned của chút thay đổi ở đây là một intkết quả của hoạt động chút CÓ THỂ intHOẶC có thể được long longtùy thuộc vào giá trị và về mặt hình thức enumkhông tương đương với một inthằng số. clang kêu gọi "như thể", gcc vẫn có âm thanh
Swift - Friday Pie

Câu trả lời:


112

Phiên bản tốt nhất là :

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

Sau đó

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

trở lại , chúng ta có thể thực hiện thủ thuật kỳ lạ này:

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

hoặc, nếu chúng ta bị mắc kẹt với , chúng ta có thể giải nó một cách đệ quy:

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 với cả 3 - bạn có thể chuyển đổi CPP_VERSION xác định và nhận được lắp ráp giống hệt nhau.

Trong thực tế, tôi sẽ sử dụng loại hiện đại nhất có thể. 14 nhịp 11 vì chúng ta không có đệ quy và do đó độ dài ký hiệu O (n ^ 2) (có thể làm bùng nổ thời gian biên dịch và sử dụng bộ nhớ trình biên dịch); 17 nhịp 14 bởi vì trình biên dịch không phải loại bỏ mã chết mảng đó, và thủ thuật mảng đó chỉ là xấu xí.

Trong số này 14 là khó hiểu nhất. Ở đây, chúng tôi tạo một mảng ẩn danh gồm tất cả các số 0, trong khi đó như một tác dụng phụ tạo ra kết quả của chúng tôi, sau đó loại bỏ mảng. Mảng bị loại bỏ có một số số 0 trong đó bằng với kích thước của gói của chúng tôi, cộng với 1 (chúng tôi thêm vào để có thể xử lý các gói trống).


Giải thích chi tiết về những gì phiên bản đang làm. Đây là một thủ thuật / hack và việc bạn phải làm điều này để mở rộng các gói tham số với hiệu quả trong C ++ 14 là một trong những lý do tại sao biểu thức gấp khi được thêm vào.

Nó được hiểu rõ nhất từ ​​trong ra ngoài:

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

điều này chỉ cập nhật rvới 1<<indexesmột chỉ mục cố định. indexeslà một gói tham số, vì vậy chúng tôi sẽ phải mở rộng nó.

Phần còn lại của công việc là cung cấp một gói tham số để mở rộng indexesbên trong.

Một bước ra:

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

ở đây chúng tôi ép kiểu biểu thức của mình thành void, cho biết chúng tôi không quan tâm đến giá trị trả về của nó (chúng tôi chỉ muốn tác dụng phụ của việc thiết lập r- trong C ++, các biểu thức như a |= bcũng trả về giá trị mà chúng đã đặta thành).

Sau đó, chúng tôi sử dụng toán tử dấu phẩy ,0để loại bỏ void"giá trị" và trả lại giá trị 0. Vì vậy, đây là một biểu thức có giá trị là 0và như một tác dụng phụ của việc tính toán 0nó đặt một bit vào r.

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

Tại thời điểm này, chúng tôi mở rộng gói tham số indexes. Vì vậy, chúng tôi nhận được:

 {
    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),
  }

trong {}. Việc sử dụng ,này không phải là toán tử dấu phẩy, mà là dấu phân tách phần tử mảng. Đây là sizeof...(indexes)+1 0s, cũng đặt các bit rnhư một hiệu ứng phụ. Sau đó, chúng tôi gán các {}hướng dẫn xây dựng mảng cho một mảng discard.

Tiếp theo, chúng tôi truyền discardđến void- hầu hết các trình biên dịch sẽ cảnh báo bạn nếu bạn tạo một biến và không bao giờ đọc nó. Tất cả các trình biên dịch sẽ không phàn nàn nếu bạn truyền nó đến void, nó giống như một cách để nói "Có, tôi biết, tôi không sử dụng cái này", vì vậy nó sẽ loại bỏ cảnh báo.


38
Xin lỗi, nhưng mã C ++ 14 là một cái gì đó. Tôi không biết điều gì.
James

14
@James Đó là một ví dụ động lực tuyệt vời về lý do tại sao các biểu thức gấp trong C ++ 17 lại rất được hoan nghênh. Nó, và các thủ thuật tương tự, hóa ra là một cách hiệu quả để mở rộng một gói "tại chỗ" mà không cần đệ quy bất kỳ và những người tuân thủ thấy dễ dàng để tối ưu hóa.
Yakk - Adam Nevraumont

4
@ruben dòng constexpr đa là bất hợp pháp ở 11
Yakk - Adam Nevraumont

6
Tôi không thể thấy mình đang kiểm tra mã C ++ 14 đó. Tôi sẽ gắn bó với C ++ 11 vì dù sao thì tôi cũng cần nó, nhưng ngay cả khi tôi có thể sử dụng nó, mã C ++ 14 yêu cầu rất nhiều lời giải thích mà tôi sẽ không làm. Các mặt nạ này luôn có thể được viết để có nhiều nhất 32 phần tử, vì vậy tôi không lo lắng về hành vi của O (n ^ 2). Rốt cuộc, nếu n bị giới hạn bởi một hằng số, thì nó thực sự là O (1). ;)
Alex Reinking

9
Đối với những người cố gắng hiểu ((1ull<<indexes)|...|0ull)nó là một "biểu thức gấp" . Cụ thể đó là một "lần nhị phân đúng" và Cần phân tích cú pháp như(pack op ... op init)
Henrik Hansen

47

Tính năng tối ưu hóa mà bạn đang tìm kiếm dường như bị bóc tách vòng lặp, được kích hoạt tại -O3hoặc theo cách thủ công -fpeel-loops. Tôi không chắc tại sao điều này lại nằm trong mục đích bóc tách vòng lặp chứ không phải là bỏ cuộn vòng lặp, nhưng có thể nó không muốn bỏ cuộn một vòng lặp với luồng điều khiển phi địa phương bên trong nó (vì có khả năng xảy ra, từ kiểm tra phạm vi).

Tuy nhiên, theo mặc định, GCC không có khả năng bóc tách tất cả các lần lặp lại, điều này rõ ràng là cần thiết. Theo thử nghiệm, việc vượt qua -O2 -fpeel-loops --param max-peeled-insns=200(giá trị mặc định là 100) sẽ hoàn thành công việc với mã gốc của bạn: https://godbolt.org/z/NNWrga


Bạn thật tuyệt vời, cảm ơn bạn! Tôi không biết điều này có thể định cấu hình trong GCC! Mặc dù vì một số lý do -O3 -fpeel-loops --param max-peeled-insns=200không thành công ... Đó là do -ftree-slp-vectorizerõ ràng.
Alex Reinking

Giải pháp này dường như được giới hạn cho mục tiêu x86-64. Đầu ra cho ARM và ARM64 vẫn chưa đẹp, điều này một lần nữa có thể hoàn toàn không phù hợp với OP.
thời gian thực

@realtime - nó thực sự có liên quan. Cảm ơn bạn đã chỉ ra rằng nó không hoạt động trong trường hợp này. Rất thất vọng khi GCC không nắm bắt được nó trước khi được hạ xuống IR dành riêng cho nền tảng. LLVM tối ưu hóa nó trước khi hạ thấp hơn nữa.
Alex Reinking

10

nếu chỉ sử dụng C ++ thì 11 phải (&a)[N]là một cách để nắm bắt các mảng. Điều này cho phép bạn viết một hàm đệ quy mà không cần sử dụng bất kỳ hàm trợ giúp nào:

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

gán nó cho một 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;
}

Kiểm tra

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

Đầu ra

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

người ta thực sự phải đánh giá cao khả năng của C ++ để tính toán bất cứ thứ gì có thể tính toán được tại thời điểm biên dịch. Nó chắc chắn vẫn làm tôi tâm trí ( <> ).


Đối với các phiên bản sau C ++ 14 và C ++ 17 , câu trả lời của yakk đã bao hàm điều đó một cách tuyệt vời.


3
Làm thế nào điều này chứng minh rằng apply_known_maskthực sự tối ưu hóa?
Alex Reinking

2
@AlexReinking: Tất cả những điều đáng sợ là vậy constexpr. Và mặc dù điều đó về mặt lý thuyết là không đủ, nhưng chúng tôi biết rằng GCC khá có khả năng đánh giá constexprnhư dự kiến.
MSalters

8

Tôi khuyến khích bạn viết một EnumSetloại thích hợp .

Viết một điều cơ bản EnumSet<E>trong C ++ 14 (trở đi) dựa trên std::uint64_tlà điều tầm thường:

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

Điều này cho phép bạn viết mã đơn giản:

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

    flags &= IMPORTANT;
}

Trong C ++ 11, nó yêu cầu một số chập, nhưng vẫn có thể xảy ra:

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

Và được gọi bằng:

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

Ngay cả GCC cũng tạo ra một andchỉ dẫn tại -O1 chốt thần :

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

2
Trong c ++ 11, nhiều constexprmã của bạn không hợp pháp. Ý tôi là, một số có 2 tuyên bố! (C ++ 11 constexpr bị hút)
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont: Bạn có nhận ra rằng tôi đã đăng 2 phiên bản mã, phiên bản đầu tiên dành cho C ++ 14 trở đi và phiên bản thứ hai được điều chỉnh đặc biệt cho C ++ 11 không? (giải thích cho những hạn chế của nó)
Matthieu M.

1
Có thể tốt hơn nếu sử dụng std :: underlying_type thay vì std :: uint64_t.
James

@James: Thực ra là không. Lưu ý rằng EnumSet<E>không sử dụng Etrực tiếp giá trị as value mà thay vào đó sử dụng 1 << e. Đó là một miền hoàn toàn khác, đó thực sự là điều làm cho lớp có giá trị như vậy => không có cơ hội vô tình lập chỉ mục bởi ethay vì 1 << e.
Matthieu M.

@MatthieuM. Vâng bạn đã đúng. Tôi đang nhầm lẫn nó với cách triển khai của chúng tôi rất giống với cách triển khai của bạn. Nhược điểm của việc sử dụng (1 << e) là nếu e nằm ngoài giới hạn cho kích thước của underlying_type thì đó có thể là UB, hy vọng là lỗi trình biên dịch.
James

7

Kể từ C ++ 11, bạn cũng có thể sử dụng kỹ thuật TMP cổ điển:

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

Liên kết đến Trình khám phá trình biên dịch: https://godbolt.org/z/Gk6KX1

Ưu điểm của cách tiếp cận này so với hàm constexpr mẫu là nó có khả năng biên dịch nhanh hơn một chút do quy tắc của Chiel .


1

Có một số ý tưởng 'thông minh' ở đây. Bạn có thể không giúp bảo trì bằng cách theo dõi chúng.

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

rất dễ viết hơn

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

?

Sau đó, không cần phần còn lại của mã.


1
Bản thân "B", "D", v.v. không phải là cờ.
Michał Łoś

Có, trước tiên bạn cần phải chuyển đổi chúng thành cờ. Điều đó không rõ ràng trong câu trả lời của tôi. lấy làm tiếc. Tôi sẽ cập nhật.
ANone
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.