1. ホーム
  2. c++

[解決済み] C++で保守性が高く、高速でコンパイル可能なビットマスクを書くにはどうしたらいいですか?

2022-10-04 17:51:13

質問

以下のようなコードを持っています。

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

このコードをどのように書けば、両方のコンパイラが正しい処理を行えるようになりますか?そうでない場合、どのように書けば、明確で、高速で、保守しやすいコードを維持できるでしょうか?

どのように解決するのですか?

最適なバージョンは 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...);
}

3つ揃ったゴッドボルト -- CPP_VERSION の定義を変更することで、同じアセンブリを得ることができます。

実際には、私はできる限り最新のものを使用します。 14 は 11 に勝ります。なぜなら、再帰がなく、それゆえ O(n^2) シンボル長 (これはコンパイル時間とコンパイラのメモリ使用量を爆発的に増やす可能性があります) があるからです。

これらのうち、14は最も混乱させるものです。 ここでは、すべての0からなる無名配列を作成し、その間に副次的効果として結果を構築し、その後配列を破棄しています。 破棄された配列には、パックのサイズと同じ数の 0 と 1 (空のパックを処理できるように追加) が入っています。


がどのようなものかを詳しく説明します。 c++14 バージョンで行っていることを詳しく説明します。 これはトリック/ハックで、C++14 でパラメータ パックを効率的に展開するためにこれを行う必要があるという事実が、fold 式が c++17 .

内側から理解するのが一番です。

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

これは単に r1<<indexes は固定インデックス用です。 indexes はパラメータパックなので、それを展開する必要があります。

残りの作業は、パラメータパックを用意して、拡張するための indexes の内部で展開するためのパラメータパックを提供することです。

一歩外に出る。

(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 0 のビットを設定することもできます。 r のビットも設定します。 次に {} 配列構築命令を配列 discard .

次に discardvoid -- ほとんどのコンパイラは、変数を作ってそれを読まなかった場合、警告を出します。 すべてのコンパイラはそれを void にキャストすれば、すべてのコンパイラは文句を言いませんが、これは一種の「はい、分かっています、私はこれを使いません"」と言う方法なので、警告は抑制されます。