C ++ में मैं कैसे बनाए रखने योग्य, तेज, संकलन-समय बिट-मास्क लिख सकता हूं?


113

मेरा कुछ कोड है जो कमोबेश इस तरह है:

#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

लेकिन जीसीसी के हर संस्करण में मैंने इसे एक बड़ी गड़बड़ी के लिए संकलित किया है जिसमें त्रुटि हैंडलिंग शामिल है जो कि सांख्यिकीय रूप से DCE'd होनी चाहिए। अन्य कोड में, यह 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?
होलीब्लैकैट

6
Enum में पहले से ही विस्तारित बिट्स के बजाय बिट पोजिशन हैं, इसलिए मैं कर सकता था(1ULL << B) | ... | (1ULL << O)
एलेक्स रिंकिंग

3
नकारात्मक पक्ष यह है कि वास्तविक नाम लंबे और अनियमित हैं और यह देखने में लगभग आसान नहीं है कि कौन से झंडे मुखौटा में हैं जो कि सभी लाइन शोर के साथ हैं।
एलेक्स रिंकिंग

4
@AlexReinking आप इसे एक बना सकते हैं (1ULL << Constant)| प्रति पंक्ति, और विभिन्न रेखाओं पर निरंतर नामों को संरेखित करें, जो आंखों पर आसान होगा।
einpoklum

मुझे लगता है कि यहाँ उपयोग न किए गए प्रकारों की कमी से संबंधित समस्या है, जीसीसी को हमेशा अतिप्रवाहित और हस्ताक्षरित / अहस्ताक्षरित हाइब्रिड में टाइप कन्वर्जन के लिए सुधार के साथ परेशानियाँ थीं। बिट शिफ्ट का intपरिणाम यहाँ बिट ऑपरेशन के परिणामस्वरूप intहोना चाहिए या long longमूल्य के आधार पर हो सकता है और औपचारिक रूप enumसे एक intस्थिर के बराबर नहीं है। क्लैंग ने "जैसे कि" के लिए कॉल किया, जीसीसी पांडित्य रहता है
स्विफ्ट - शुक्रवार पाई

जवाबों:


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

सभी 3 के साथ Godbolt - आप CPP_VERSION को परिभाषित कर सकते हैं, और समान असेंबली प्राप्त कर सकते हैं।

व्यवहार में मैं सबसे आधुनिक का उपयोग कर सकता था। 14 बीट्स 11, क्योंकि हमारे पास रिकर्सन नहीं है और इसलिए O (n ^ 2) सिंबल लेंथ (जो कंपाइल टाइम और कंपाइलर मेमोरी यूज को विस्फोट कर सकता है); 17 बीट्स 14 क्योंकि कंपाइलर को उस एरे को डेड-कोड-खत्म करने की ज़रूरत नहीं है, और यह ऐरे ट्रिक सिर्फ बदसूरत है।

इनमें से 14 सबसे भ्रमित करने वाला है। यहां हम सभी 0s का एक अनाम सरणी बनाते हैं, इस बीच एक साइड इफेक्ट हमारे परिणाम का निर्माण करता है, फिर सरणी को छोड़ दें। त्याग किए गए सरणी में हमारे पैक के आकार के बराबर 0 की संख्या है, प्लस 1 (जिसे हम जोड़ते हैं ताकि हम अन्य पैक को संभाल सकें)।


विस्तृत विवरण क्या है संस्करण कर रहा है। यह एक ट्रिक / हैक है, और आपको C ++ 14 में दक्षता के साथ पैरामीटर्स पैक्स का विस्तार करने के लिए ऐसा करना होगा, यह एक कारण है कि जहाँ अभिव्यक्ति को जोड़ा जाता है, वहां फोल्ड क्यों किया जाता है?

यह अंदर से सबसे अच्छी तरह से समझा जाता है:

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

यह केवल एक निश्चित सूचकांक के rसाथ अद्यतन करता है 1<<indexesindexesएक पैरामीटर पैक है, इसलिए हमें इसका विस्तार करना होगा।

बाकी का काम indexesअंदर के विस्तार के लिए एक पैरामीटर पैक प्रदान करना है।

एक कदम बाहर:

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

यहां हमने अपनी अभिव्यक्ति को voidदर्शाया है , यह दर्शाता है कि हम इसके वापसी मूल्य के बारे में परवाह नहीं करते हैं (हम बस सेटिंग के साइड इफेक्ट चाहते हैं r- सी ++ में, अभिव्यक्ति जैसे a |= bकि वे निर्धारित मूल्य भी वापस करते हैंa करते हैं)।

फिर हम कॉमा ऑपरेटर का उपयोग करते हैं ,और "मूल्य" 0को त्यागने के लिए void, और मूल्य को वापस करते हैं 0। तो यह एक अभिव्यक्ति है जिसका मूल्य है 0और 0इसकी गणना के साइड इफेक्ट के रूप में थोड़ा सा सेट करता है r

  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 0एस है, जो rसाइड इफेक्ट के रूप में भी बिट्स सेट करता है। हम तब {}सरणी निर्माण निर्देशों को एक सरणी में असाइन करते हैं discard

अगला हम डाली discardको void- अगर आप एक चर बनाने के सबसे compilers आपको चेतावनी देगा और इसे पढ़ा नहीं है। यदि आप इसे डालते हैं तो सभी संकलक शिकायत नहीं करेंगेvoid , तो यह कहने का एक तरीका है "हां, मुझे पता है, मैं इसका उपयोग नहीं कर रहा हूं", इसलिए यह चेतावनी को दबा देता है।


38
क्षमा करें, लेकिन वह C ++ 14 कोड कुछ है। मैं नहीं जानता कि क्या।
जेम्स

14
@ जेम्स यह एक अद्भुत प्रेरक उदाहरण है कि C ++ 17 में फोल्ड एक्सप्रेशन बहुत स्वागत योग्य क्यों हैं। यह, और इसी तरह की चाल, बिना किसी पुनरावृत्ति के "पैक" का विस्तार करने के लिए एक कुशल तरीका हो सकता है और यह कि कंपाइलर को अनुकूलित करना आसान लगता है।
यक्क - एडम नेवरामोंट

4
@ruben बहु लाइन constexpr 11 में गैर कानूनी है
Yakk - एडम Nevraumont

6
मैं खुद को उस C ++ 14 कोड में जाँच नहीं कर सकता। मैं वैसे भी C ++ 11 से चिपका रहूंगा, वैसे भी, लेकिन अगर मैं इसका उपयोग कर सकता हूं, तो C ++ 14 कोड को बहुत स्पष्टीकरण की आवश्यकता होगी जो मैं नहीं करूंगा। इन मास्क को हमेशा 32 तत्वों पर लिखा जा सकता है, इसलिए मुझे O (n ^ 2) व्यवहार के बारे में चिंता नहीं है। आखिरकार, यदि n एक निरंतरता से घिरा है, तो यह वास्तव में O (1) है। ;)
एलेक्स रिंकिंग

9
((1ull<<indexes)|...|0ull)इसे समझने की कोशिश करने वालों के लिए यह एक "गुना अभिव्यक्ति" है । विशेष रूप से यह "बाइनरी राइट फोल्ड" है और इसे पार्स किया जाना चाहिए(pack op ... op init)
हेनरिक हैनसेन

47

आप जिस अनुकूलन की तलाश कर रहे हैं, वह लूप पीलिंग लगता है, जो सक्षम है -O3, या मैन्युअल रूप से -fpeel-loops। मुझे यकीन नहीं है कि यह क्यों लूप के अनियंत्रित होने के बजाय लूप पीलिंग के दायरे में आता है, लेकिन संभवतः यह लूप को इसके अंदर गैर-फोकल नियंत्रण प्रवाह के साथ अनियंत्रित करने के लिए तैयार नहीं है (जैसा कि, संभवतः, सीमा की जांच से)।

डिफ़ॉल्ट रूप से, हालांकि, जीसीसी सभी पुनरावृत्तियों को छीलने में सक्षम होने से कम रोकता है, जो स्पष्ट रूप से आवश्यक है। प्रायोगिक तौर पर, -O2 -fpeel-loops --param max-peeled-insns=200(डिफ़ॉल्ट मान 100 है) आपके मूल कोड के साथ काम किया जाता है: https://godbolt.org/z/NNWrga


आप अद्भुत हैं धन्यवाद! मुझे नहीं पता था कि यह जीसीसी में कॉन्फ़िगर करने योग्य था! हालांकि किसी कारण के लिए -O3 -fpeel-loops --param max-peeled-insns=200विफल रहता है ... यह -ftree-slp-vectorizeस्पष्ट रूप से होने के कारण है ।
एलेक्स रिंकिंग

यह समाधान x86-64 लक्ष्य तक सीमित लगता है। एआरएम और एआरएम 64 के लिए आउटपुट अभी भी सुंदर नहीं है, जो फिर ओपी के लिए पूरी तरह अप्रासंगिक हो सकता है।
रीयलटाइम

@ वास्तविक समय - यह वास्तव में कुछ हद तक प्रासंगिक है। यह इंगित करने के लिए धन्यवाद कि यह इस मामले में काम नहीं करता है। बहुत निराशाजनक है कि जीसीसी इसे प्लेटफॉर्म-विशिष्ट आईआर पर उतारे जाने से पहले नहीं पकड़ता है। एलएलवीएम आगे किसी भी कम होने से पहले इसे अनुकूलित करता है
एलेक्स रिंकिंग

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 याक का उत्तर पहले से ही आश्चर्यजनक रूप से शामिल है।


3
यह कैसे प्रदर्शित करता है जो apply_known_maskवास्तव में अनुकूलन करता है?
एलेक्स रिंकिंग

2
@AlexReinking: सभी डरावने बिट हैं constexpr। और जबकि यह सैद्धांतिक रूप से पर्याप्त नहीं है, हम जानते हैं कि GCC मूल्यांकन के लिए काफी सक्षम है constexpr
एमएसल्टर्स

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 में, इसके लिए कुछ दृढ़ संकल्पों की आवश्यकता होती है, लेकिन फिर भी यह संभव है:

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 गॉडबोल्ट में एक निर्देश दिया है :

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

2
में c ++ 11 अपने बारे में ज्यादा constexprकोड कानूनी नहीं है। मेरा मतलब है, कुछ में 2 कथन हैं! (सी ++ 11 कॉन्स्ट्रेक्स चूसा)
यक्क - एडम नेवरुमोंट

@ यक्क-आदमनेवरुमोंट: आपको एहसास हुआ कि मैंने कोड के 2 संस्करण पोस्ट किए हैं , सी ++ 14 के लिए पहला, और दूसरा विशेष रूप से सी ++ 11 के लिए सिलवाया गया है? (अपनी सीमाओं को ध्यान में रखते हुए)
Matthieu M.

1
एसटीडी का उपयोग करना बेहतर हो सकता है :: एसटीडी के बजाय अंतर्निहित_टाइप :: uint64_t।
जेम्स

@ जेम्स: वास्तव में, नहीं। ध्यान दें कि EnumSet<E>मूल्य के Eरूप में सीधे मूल्य का उपयोग न करें , बल्कि उपयोग करता है 1 << e। यह पूरी तरह से एक अलग डोमेन है, जो वास्तव में वर्ग को इतना मूल्यवान बनाता है => eइसके बजाय गलती से अनुक्रमण करने का कोई मौका नहीं 1 << e
Matthieu एम।

@MatthieuM। हाँ तुम सही हो। मैं इसे हमारे अपने कार्यान्वयन के साथ भ्रमित कर रहा हूं जो आपके लिए बहुत समान है। उपयोग करने का नुकसान (1 << ई) यह है कि यदि ई अंतर्निहित_टाइप के आकार के लिए सीमा से बाहर है तो यह संभवतः यूबी है, उम्मीद है कि एक संकलक त्रुटि।
जेम्स

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

कंपाइलर एक्सप्लोरर से लिंक: https://godbolt.org/z/Gk6KX1

टेम्पलेट कॉन्स्ट्रेप फ़ंक्शन पर इस दृष्टिकोण का लाभ यह है कि यह चील के शासन के कारण संकलित करने के लिए संभावित रूप से थोड़ा तेज़ है ।


1

यहाँ 'चतुर' विचारों के लिए कुछ दूर हैं। आप शायद उनका अनुसरण करके स्थिरता बनाए रखने में मदद नहीं कर रहे हैं।

है

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

लिखने की तुलना में बहुत आसान है

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

?

फिर बाकी किसी भी कोड की जरूरत नहीं है।


1
"बी", "डी", आदि स्वयं झंडे नहीं हैं।
मिशैल śoś

हां, आपको पहले इन झंडों को बदलने की जरूरत होगी। मेरे जवाब में यह बिल्कुल स्पष्ट नहीं है। माफ़ करना। मैं अपडेट कर दूंगा।
12
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.