多かれ少なかれこのようないくつかのコードがあります。
#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...);
}
_
つすべてのゴッドボルト -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
に割り当てます。
次に、discard
をvoid
にキャストします。ほとんどのコンパイラーは、変数を作成してそれを読み取らないと警告を表示します。すべてのコンパイラは、void
にキャストしても文句を言うことはありません。これは、「はい、知っています。これを使用していません」と言っているので、警告を抑制します。
探している最適化はループ剥離のようで、-O3
で有効にするか、-fpeel-loops
で手動で有効にします。なぜこれがループの展開ではなくループの剥離の範囲に入るのかはわかりませんが、おそらく内部に非ローカル制御フローがあるループを展開することは望んでいません(範囲チェックから可能性として)。
ただし、デフォルトでは、GCCはすべての反復をピールできなくなるのを止めますが、これは明らかに必要です。実験的に、-O2 -fpeel-loops --param max-peeled-insns=200
(デフォルト値は100)を渡すと、元のコードで作業が完了します。 https://godbolt.org/z/NNWrga
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がそれを素晴らしくカバーしています。
適切な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でさえ、-O1
でand
命令を簡単に生成します- godbolt :
apply_known_mask(EnumSet<Flags>&):
and QWORD PTR [rdi], 775946532
ret
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 が原因でコンパイルが少し速くなる可能性があることです。
ここには「賢い」アイデアには遠いところがあります。あなたはおそらくそれらに従うことによって保守性を助けていないでしょう。
は
{B, D, E, H, K, M, L, O};
書くよりずっと簡単
(B| D| E| H| K| M| L| O);
?
その後、残りのコードは必要ありません。