C ++ 'da bakımı yapılabilir, hızlı, derleme zamanı bit maskesi nasıl yazabilirim?


113

Aşağı yukarı bunun gibi bir kodum var:

#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 akıllıca bir şey yapar ve bunu tek bir andtalimatta derler (daha sonra her yerde satır içi olur):

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

Ancak denediğim her GCC sürümü, bunu statik olarak DCE olması gereken hata işlemeyi içeren muazzam bir karmaşaya derliyor. Diğer kodda, important_bitsmuadili veri olarak kodla aynı hizaya bile yerleştirecektir !

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

Her iki derleyicinin de doğru şeyi yapabilmesi için bu kodu nasıl yazmalıyım? Aksi takdirde, açık, hızlı ve sürdürülebilir kalması için bunu nasıl yazmalıyım?


4
Bir döngü kullanmak yerine, ile bir maske oluşturamaz B | D | E | ... | Omısınız?
HolyBlackCat

6
(1ULL << B) | ... | (1ULL << O)
Numaralandırma

3
Olumsuz yanı, gerçek adların uzun ve düzensiz olması ve tüm bu satır gürültüsüyle maskede hangi bayrakların olduğunu görmek neredeyse kolay değildir.
Alex Reinking

4
@AlexReinking Bir tane yapabilirsiniz (1ULL << Constant)| her satırda ve sabit isimleri farklı satırlarda hizalayın, bu gözler için daha kolay olacaktır.
einpoklum

Buradaki sorunun işaretsiz tipin kullanılmaması ile ilgili olduğunu düşünüyorum, GCC her zaman işaretli / işaretsiz hibridde taşma ve tip dönüşümü için statik olarak düzeltme düzeltme konusunda sorunlar yaşadı.Buradaki bit kaymasının intsonucu, bit işleminin bir sonucudur intVEYA long longdeğere bağlı olabilir ve resmi enumolarak bir intsabite eşdeğer değildir . clang "sanki" anlamına gelir, gcc bilgiçlik taslamaz
Swift - Friday Pie

Yanıtlar:


112

En iyi versiyon :

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

Sonra

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

geri , bu garip numarayı yapabiliriz:

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

ya da sıkışırsak , bunu yinelemeli olarak çözebiliriz:

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 ile 3 - CPP_VERSION tanımını değiştirebilir ve aynı montajı elde edebilirsiniz.

Pratikte elimden gelenin en modernini kullanırdım. 14, 11'i atar çünkü özyineleme ve dolayısıyla O (n ^ 2) sembol uzunluğumuz (derleme zamanını ve derleyici bellek kullanımını patlatabilir); 17, 14'ü atar çünkü derleyicinin bu diziyi ölü kodla ortadan kaldırması gerekmez ve bu dizi hilesi çirkin.

Bunlardan 14'ü en kafa karıştırıcı. Burada tüm 0'ların anonim bir dizisini oluşturuyoruz, bu arada bir yan etki olarak sonucumuzu oluşturuyoruz ve sonra diziyi atıyoruz. Atılan dizinin içinde, paketimizin boyutuna eşit sayıda 0, artı 1 (boş paketleri ele alabilmemiz için ekliyoruz) vardır.


Ne olduğuna dair ayrıntılı bir açıklama sürüm yapıyor. Bu bir hile / hack'tir ve parametre paketlerini C ++ 14'te verimli bir şekilde genişletmek için bunu yapmanız gerektiği gerçeği, katlama ifadelerinin eklenmesinin nedenlerinden biridir..

En iyi içten dışa anlaşılır:

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

bu sadece sabit bir dizin için rile güncellenir 1<<indexes. indexesbir parametre paketidir, bu yüzden onu genişletmemiz gerekecek.

İşin geri kalanı, indexesiçine genişletilecek bir parametre paketi sağlamaktır .

Bir adım dışarı:

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

burada ifademizi void, dönüş değerini umursamadığımızı gösterecek şekilde çeviriyoruz (sadece ayarlamanın yan etkisini istiyoruz r- C ++ 'da, gibi ifadeler a |= bde ayarladıkları değeri döndürüyor a).

Sonra virgül operatörünü kullanırız ,ve "değeri" 0atıp voiddeğeri döndürürüz 0. Yani bu, değeri olan 0ve 0onu hesaplamanın bir yan etkisi olan bir ifadedir r.

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

Bu noktada, parametre paketini genişletiyoruz indexes. Böylece şunu elde ederiz:

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

içinde {}. Bu kullanımı ,olan değil virgül operatörü ziyade dizi öğesi ayırıcı. Bu, sizeof...(indexes)+1 0aynı zamanda rbir yan etki olarak bitleri de ayarlayan s'dir . Daha sonra {}dizi oluşturma talimatlarını bir diziye atarız discard.

Kadroya Sonraki discardetmek void- birçok derleyici bir değişken oluşturmak eğer sizi uyarır ve okumak asla. Eğer onu yayınlarsanız tüm derleyiciler şikayet etmeyecektir void, bu "Evet, biliyorum, bunu kullanmıyorum" demenin bir yoludur, bu yüzden uyarıyı bastırır.


38
Üzgünüm ama bu C ++ 14 kodu bir şey. Ne olduğunu bilmiyorum.
James

14
@James C ++ 17'deki kıvrım ifadelerinin neden çok hoş karşılandığına dair harika bir motive edici örnek. Bu ve benzer hileler, herhangi bir özyineleme olmaksızın bir paketi "yerinde" genişletmenin etkili bir yolu olduğu ve karşılaştırıcıların optimize etmeyi kolay bulduğu ortaya çıktı.
Yakk - Adam Nevraumont

4
@ruben multi line constexpr 11'de yasa dışı
Yakk - Adam Nevraumont

6
Kendimi o C ++ 14 kodunu kontrol ederken göremiyorum. Yine de ihtiyacım olduğu için C ++ 11'e sadık kalacağım, ancak kullanabilsem bile, C ++ 14 kodu çok fazla açıklama gerektirmez. Bu maskeler her zaman en fazla 32 öğeye sahip olacak şekilde yazılabilir, bu nedenle O (n ^ 2) davranışı konusunda endişelenmiyorum. Sonuçta, n bir sabitle sınırlanmışsa, o zaman gerçekten O (1) olur. ;)
Alex Reinking

9
Anlamaya çalışanlar için bu ((1ull<<indexes)|...|0ull)bir "kıvrımlı ifade" dir . Özellikle "ikili bir sağ kıvrım" ve şu şekilde ayrıştırılmalıdır(pack op ... op init)
Henrik Hansen

47

Aradığınız optimizasyon -O3, içinde veya manuel olarak etkinleştirilen döngü soyma gibi görünüyor -fpeel-loops. Bunun neden döngü açma yerine döngü soyma kapsamına girdiğinden emin değilim, ancak muhtemelen içinde yerel olmayan kontrol akışı olan bir döngüyü açmak istemez (potansiyel olarak aralık kontrolünden olduğu gibi).

Varsayılan olarak, yine de GCC, görünüşe göre gerekli olan tüm yinelemeleri soyabilmek için kısa sürede durur. Deneysel olarak, geçmek -O2 -fpeel-loops --param max-peeled-insns=200(varsayılan değer 100'dür) işi orijinal kodunuzla bitirir: https://godbolt.org/z/NNWrga


Sen şaşırtıcı teşekkür ederim! Bunun GCC'de yapılandırılabilir olduğu hakkında hiçbir fikrim yoktu! Bazı nedenlerden dolayı -O3 -fpeel-loops --param max-peeled-insns=200başarısız olsa da ... -ftree-slp-vectorizeGörünüşe göre yüzünden .
Alex Reinking

Bu çözüm, x86-64 hedefi ile sınırlı görünüyor. ARM ve ARM64 için çıktı hala hoş değil, bu durumda yine OP için tamamen alakasız olabilir.
gerçek zamanlı

@realtime - aslında biraz alakalı. Bu durumda işe yaramadığını belirttiğiniz için teşekkürler. GCC'nin platforma özel bir IR'ye indirilmeden önce onu yakalamaması çok hayal kırıklığı yaratıyor. LLVM, daha fazla düşürmeden önce onu optimize eder .
Alex Reinking

10

sadece C ++ 11 kullanmak (&a)[N]dizileri yakalamanın bir yoludur. Bu, yardımcı işlevleri kullanmadan tek bir özyinelemeli işlev yazmanıza olanak tanır:

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

bir şuna atamak 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;
}

Ölçek

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

Çıktı

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

C ++ 'ın derleme zamanında hesaplanabilen her şeyi hesaplama yeteneğini gerçekten takdir etmek gerekir. Kesinlikle hala aklımı uçuruyor ( <> ).


Daha sonraki sürümler için C ++ 14 ve C ++ 17 yakk'ın cevabı zaten harika bir şekilde bunu kapsıyor.


3
Bu, bunun apply_known_maskgerçekten optimize olduğunu nasıl gösterir ?
Alex Reinking

2
@AlexReinking: Tüm korkutucu kısımlar constexpr. Ve bu teorik olarak yeterli olmasa da, GCC'nin constexpramaçlandığı gibi değerlendirme yapabileceğini biliyoruz .
MSalters

8

Size uygun bir EnumSettip yazmanızı tavsiye ederim .

EnumSet<E>C ++ 14'te (sonrası) temel alan bir temel yazmak std::uint64_tönemsizdir:

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

Bu, basit kod yazmanıza izin verir:

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

    flags &= IMPORTANT;
}

C ++ 11'de bazı kıvrımlar gerektirir, ancak yine de mümkündür:

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

Ve şununla çağrılır:

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 bile önemsiz bir şekilde godbolt'ta bir andtalimat oluşturur :-O1

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

2
In c ++ 11 senin çok constexprkod yasal değildir. Demek istediğim, bazılarının 2 ifadesi var! (C ++ 11 constexpr emildi)
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont: Kodun 2 versiyonunu yayınladığımı fark ettiniz mi , ilki C ++ 14 için ve ikincisi özel olarak C ++ 11 için uyarlanmış? (sınırlamalarını hesaba katmak için)
Matthieu M.

1
Std :: uint64_t yerine std :: underlying_type kullanmak daha iyi olabilir.
James

@James: Aslında hayır. Bunun doğrudan EnumSet<E>değer Eolarak kullanılmadığını , bunun yerine kullandığını unutmayın 1 << e. Tamamen farklı bir alandır, bu da sınıfı bu kadar değerli kılan şeydir => ebunun yerine yanlışlıkla indeksleme şansı yoktur 1 << e.
Matthieu M.

@MatthieuM. Evet haklısın. Bunu, sizinkine çok benzeyen kendi uygulamamızla karıştırıyorum. (1 << e) kullanmanın dezavantajı, e'nin temel_tür boyutu için sınırların dışında olması durumunda muhtemelen UB, umarız bir derleyici hatasıdır.
James

7

C ++ 11'den beri klasik TMP tekniğini de kullanabilirsiniz:

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

Derleyici Gezgini'ne bağlantı: https://godbolt.org/z/Gk6KX1

Bu yaklaşımın şablon constexpr işlevine göre avantajı, Chiel kuralı nedeniyle derlemenin potansiyel olarak biraz daha hızlı olmasıdır .


1

Burada çok uzak fikirler var. Muhtemelen onları takip ederek sürdürülebilirliğe yardımcı olmuyorsunuz.

dır-dir

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

yazmaktan çok daha kolay

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

?

O zaman kodun geri kalanının hiçbirine gerek kalmaz.


1
"B", "D" vb. Kendileri bayrak değildir.
Michał Łoś

Evet, önce bunları bayraklara dönüştürmeniz gerekir. Cevabımda bu hiç net değil. afedersiniz. Güncelleyeceğim.
BİR
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.