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

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

両方のコンパイラが正しいことをできるようにするには、このコードをどのように記述すればよいですか?それが失敗した場合、これを明確にして高速で保守可能な状態に保つには、どのようにこれを書けばよいですか?


4
ループを使用するのではなく、でマスクを作成できませんB | D | E | ... | Oか?
HolyBlackCat

6
enumにはすでに拡張されたビットではなくビット位置があるので、私はできる(1ULL << B) | ... | (1ULL << O)
Alex Reinking

3
欠点は、実際の名前は長く不規則であり、すべてのラインノイズが含まれているマスクにどのフラグがあるかを確認するのはそれほど簡単ではないということです。
Alex Reinking

4
@AlexReinkingあなたはそれを1つにすることができます(1ULL << Constant)| 行ごとに、定数名を別の行に配置すると、見やすくなります。
einpoklum

私はここで使用unsigned型、静的ここでビットシフトの署名/符号なしhybrid.Resultに溢れと型変換のための補正を捨てるとGCCは常に持っていた問題の欠如に関連する問題があると思わintビット演算の結果ができるintORすることができる long long値に応じて、正式にenumint定数と同等ではありません。clangは「あたかも」を要求し、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...);
}

3つすべてのGodbolt -CPP_VERSIONの定義を切り替えて、同じアセンブリを取得できます。

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

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


何の詳細な説明 バージョンがやっています。これはトリック/ハックであり、C ++ 14で効率的にパラメーターパックを拡張するためにこれを実行する必要があるという事実は、フォールド式が追加される理由の1つです。

それは完全に理解されています:

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

これは単なる更新r1<<indexes固定インデックスのため。 indexesパラメータパックなので、拡張する必要があります。

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

1つのステップ:

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

ここでは、式をにキャストしてvoid、戻り値を気にしないことを示しています(設定の副作用が必要なだけですr-C ++では、式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 0sでありr、副作用としてビットも設定されます。次に、{}配列構築命令を配列に割り当てdiscardます。

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


38
申し訳ありませんが、そのC ++ 14コードは何かです。何なのかわかりません。
ジェームズ

14
@James C ++ 17の折り畳み式が非常に歓迎される理由の素晴らしい動機付けの例です。これと同様のトリックは、再帰なしで「インプレース」でパックを拡張するための効率的な方法であり、コンパイラーは最適化が容易であることがわかります。
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)にとっては「折りたたみ式」です。具体的には、「バイナリの右折り」であり、次のように解析する必要があります(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では、いくつかの畳み込みが必要ですが、それでも可能です。

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 ++ 11あなたの多くのconstexprコードは、法的ではありません。つまり、いくつかは2つのステートメントを持っています!(C ++ 11 constexpr sucked)
Yakk-Adam Nevraumont

@ Yakk-AdamNevraumont:私が2つのバージョンのコードを投稿したことをご存知でしたか。最初はC ++ 14以降のもので、もう1 はC ++ 11用に特別に調整されたものです。(その制限を説明するため)
Matthieu M.

1
std :: uint64_tの代わりにstd :: underlying_typeを使用する方がよい場合があります。
ジェームズ

@ジェームズ:実は違う。EnumSet<E>は値としてEasの値を直接使用せず、代わりにを使用することに注意してください1 << e。それは完全に異なるドメインであり、それが実際にクラスを非常に価値のあるものにしている=>のe代わりにによって誤ってインデックスを作成する可能性はありません1 << e
Matthieu M.

@MatthieuM。はい、あなたが正しい。私はそれをあなたのものと非常に似ている私たち自身の実装と混同しています。(1 << e)を使用することの欠点は、eがunderground_typeのサイズの範囲外の場合、おそらくUBであり、おそらくコンパイラエラーであることです。
James

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関数に対するこのアプローチの利点は、Chielのルールにより、コンパイルがわずかに高速になる可能性があることです


1

ここには「賢い」アイデアがいくつかあります。あなたはおそらくそれらに従うことによって保守性を助けていません。

です

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

書くよりもずっと簡単

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

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


1
「B」、「D」などはフラグ自体ではありません。
のMichałロサンゼルス

はい、まずこれらをフラグに変換する必要があります。それは私の答えではまったく明確ではありません。ごめんなさい。更新します。
アノン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.