web-dev-qa-db-ja.com

保守可能で高速なコンパイル時ビットマスクをC ++で書くにはどうすればよいですか?

多かれ少なかれこのようないくつかのコードがあります。

#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 はスマートなことを行い、これを単一の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*, ...)

両方のコンパイラが正しいことを行えるように、このコードをどのように書くべきですか?それに失敗したら、これをどのように書いて、それが明確で、高速で、保守可能であるようにするべきですか?

113
Alex Reinking

最良のバージョンは c ++ 17

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

c ++ 14 に戻り、この奇妙なトリックを行うことができます。

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

または、 c ++ 11 で立ち往生している場合、再帰的に解決できます。

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

つすべてのゴッドボルト -CPP_VERSION定義を切り替えて、同一のアセンブリを取得できます。

実際には、できる限り最新のものを使用します。再帰がなく、したがってO(n ^ 2)シンボルの長さ(コンパイル時間とコンパイラーのメモリ使用量が爆発する可能性がある)がないため、14ビート11。コンパイラーがその配列をデッドコードで除去する必要がないため、17は14を上回り、その配列のトリックはtrickいだけです。

これらのうち、14が最も混乱しています。ここでは、すべて0の匿名配列を作成しますが、副作用として結果を構築し、その配列を破棄します。破棄された配列には、パックのサイズに等しい0の数に1を加えたもの(空のパックを処理できるように追加します)があります。


c ++ 14 バージョンの動作の詳細な説明。これはトリック/ハックであり、C++ 14で効率的にパラメーターパックを展開するためにこれを行う必要があるという事実は、 c ++ 17 でfold式が追加された理由の1つです。

それは内側から最もよく理解されています:

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

これは、固定変数のrを_1<<indexes_で更新するだけです。 indexesはパラメーターパックなので、展開する必要があります。

残りの作業は、内部でindexesを展開するパラメーターパックを提供することです。

1つのステップアウト:

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

ここで、式をvoidにキャストし、その戻り値を気にしないことを示します(C++では、rの設定の副作用が必要です。_a |= b_などの式も、aに設定した値を返します)。

次に、カンマ演算子_,_および_0_を使用してvoid "value"を破棄し、値_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),
  }
_

_{}_で。この_,_の使用は、notコンマ演算子ではなく、配列要素の区切り記号です。これはsizeof...(indexes)+1 _0_ sであり、副作用としてrのビットも設定します。次に、_{}_配列構築命令を配列discardに割り当てます。

次に、discardvoidにキャストします。ほとんどのコンパイラーは、変数を作成してそれを読み取らないと警告を表示します。すべてのコンパイラは、voidにキャストしても文句を言うことはありません。これは、「はい、知っています。これを使用していません」と言っているので、警告を抑制します。

112

探している最適化はループ剥離のようで、-O3で有効にするか、-fpeel-loopsで手動で有効にします。なぜこれがループの展開ではなくループの剥離の範囲に入るのかはわかりませんが、おそらく内部に非ローカル制御フローがあるループを展開することは望んでいません(範囲チェックから可能性として)。

ただし、デフォルトでは、GCCはすべての反復をピールできなくなるのを止めますが、これは明らかに必要です。実験的に、-O2 -fpeel-loops --param max-peeled-insns=200(デフォルト値は100)を渡すと、元のコードで作業が完了します。 https://godbolt.org/z/NNWrga

47
Sneftel

c ++ 11のみを使用する必要がある場合は、(&a)[N]が配列をキャプチャする方法です。これにより、ヘルパー関数をまったく使用せずに、1つの再帰関数を作成できます。

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's answerがそれを素晴らしくカバーしています。

10
Stack Danny

適切なEnumSet型を書くことをお勧めします。

EnumSet<E>に基づく基本的なstd::uint64_tをC++ 14(以降)で書くのは簡単です:

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でさえ、-O1and命令を簡単に生成します- godbolt

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

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関数に対するこのアプローチの利点は、 rule of Chiel が原因でコンパイルが少し速くなる可能性があることです。

7
Michał Łoś

ここには「賢い」アイデアには遠いところがあります。あなたはおそらくそれらに従うことによって保守性を助けていないでしょう。

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

書くよりずっと簡単

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

その後、残りのコードは必要ありません。

1
ANone