【问题标题】:How do I write a maintainable, fast, compile-time bit-mask in C++?如何在 C++ 中编写可维护、快速、编译时的位掩码?
【发布时间】:2019-02-20 12:20:36
【问题描述】:

我有一些或多或少像这样的代码:

#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

但是every version of GCC I've tried 将其编译成一个巨大的混乱,其中包括应该静态 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*, ...)

我应该如何编写这段代码,以便两个编译器都能做正确的事情?如果做不到这一点,我应该如何编写它才能保持清晰、快速和可维护?

【问题讨论】:

  • 不使用循环,就不能用B | D | E | ... | O构造掩码吗?
  • 枚举有位位置而不是已经扩展的位,所以我可以做(1ULL &lt;&lt; B) | ... | (1ULL &lt;&lt; O)
  • 缺点是实际名称很长且不规则,并且很难看到掩码中有哪些标志以及所有线条噪音。
  • @AlexReinking 你可以成为一个(1ULL &lt;&lt; Constant) |每行,并在不同的行上对齐常量名称,这样会更容易。
  • 我认为这里的问题与缺乏使用无符号类型有关,GCC 总是在有符号/无符号混合中静态丢弃溢出和类型转换的校正时遇到问题。这里的位移结果是 int 结果位操作的可能是int 或可能是long long,具体取决于值,并且正式的enum 不等同于int 常量。 clang 呼吁“好像”,gcc 保持迂腐

标签: c++ c++11 bit-manipulation


【解决方案1】:

最好的版本是:

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

Godbolt with all 3 -- 您可以切换 CPP_VERSION 定义,并获得相同的程序集。

在实践中,我会尽可能使用最现代的。 14 比 11 好,因为我们没有递归,因此没有 O(n^2) 符号长度(这会增加编译时间和编译器内存使用量); 17 比 14 好,因为编译器不必对那个数组进行死代码消除,而且那个数组技巧很丑。

这 14 个是最令人困惑的。在这里,我们创建一个全 0 的匿名数组,同时作为副作用构造我们的结果,然后丢弃该数组。丢弃的数组中有许多 0,等于我们包的大小加上 1(我们添加它以便我们可以处理空包)。


版本在做什么的详细说明。这是一个技巧/hack,您必须这样做才能在 C++14 中高效地扩展参数包,这也是为什么在 中添加折叠表达式的原因之一。

最好由内而外理解:

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

这只是将r 更新为1&lt;&lt;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)+10s,它还设置了r 中的位作为副作用。然后我们将{}数组构造指令分配给数组discard

接下来我们将discard 转换为void——如果你创建了一个变量并且从不读取它,大多数编译器都会警告你。如果您将其转换为void,所有编译器都不会抱怨,这是一种说“是的,我知道,我没有使用它”的方式,因此它会抑制警告。

【讨论】:

  • 抱歉,C++14 代码有点意思。我不知道是什么。
  • @James 这是一个很好的激励示例,说明了为什么 C++17 中的折叠表达式非常受欢迎。事实证明,它和类似的技巧是一种无需任何递归即可“就地”扩展包的有效方法,并且编译器很容易优化。
  • @ruben 多行 constexpr 在 11 中是非法的
  • 我看不到自己签入该 C++14 代码。无论如何,我会坚持使用 C++11,因为我需要它,但即使我可以使用它,C++14 代码也需要太多的解释,我不会。这些掩码总是可以写成最多有 32 个元素,所以我不担心 O(n^2) 的行为。毕竟,如果 n 以常数为界,那么它实际上是 O(1)。 ;)
  • 对于那些试图理解((1ull&lt;&lt;indexes)|...|0ull)的人来说,它是"fold expression"。具体来说,它是一个“二进制右折叠”,它应该被解析为(packop...opinit)
【解决方案2】:

您正在寻找的优化似乎是循环剥离,它在-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
  • 此解决方案似乎仅限于 x86-64 目标。 ARM 和 ARM64 的输出仍然不漂亮,这可能与 OP 完全无关。
  • @realtime - 实际上有点相关。感谢您指出它在这种情况下不起作用。非常令人失望的是 GCC 在降低到特定于平台的 IR 之前没有抓住它。 LLVM optimizes it 在进一步降低之前。
【解决方案3】:

如果只能使用 C++11,(&amp;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's 答案已经很好地涵盖了这一点。

【讨论】:

  • 这如何证明apply_known_mask 确实优化了?
  • @AlexReinking:所有可怕的部分都是constexpr。虽然理论上这还不够,但我们知道 GCC 完全有能力按预期评估 constexpr
【解决方案4】:

我鼓励你写一个正确的EnumSet 类型。

基于 std::uint64_t 在 C++14(以后)中编写基本的 EnumSet&lt;E&gt; 很简单:

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

And 被调用:

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 godbolt 处生成一条 and 指令:

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

【讨论】:

  • c++11 中,您的大部分constexpr 代码都是不合法的。我的意思是,有些有 2 个语句! (C++11 constexpr 很烂)
  • @Yakk-AdamNevraumont:你确实意识到我发布了 2 个版本的代码,第一个版本用于 C++14,第二个版本专门为 C++ 定制11? (考虑到它的局限性)
  • 使用 std::underlying_type 代替 std::uint64_t 可能会更好。
  • @James:实际上,没有。请注意EnumSet&lt;E&gt; 不直接使用E 作为值,而是使用1 &lt;&lt; e。这是一个完全不同的领域,这实际上是使该类如此有价值的原因 => 没有机会意外地被 e 而不是 1 &lt;&lt; e 索引。
  • @MatthieuM。你是对的。我将它与我们自己的实现混淆了,这与您的实现非常相似。使用 (1
【解决方案5】:

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

编译器资源管理器链接:https://godbolt.org/z/Gk6KX1

这种方法相对于模板 constexpr 函数的优势在于,由于rule of Chiel,它的编译速度可能稍快。

【讨论】:

    【解决方案6】:

    这里有一些“聪明”的想法。关注它们可能对可维护性没有帮助。

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

    比写起来容易多了

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

    ?

    那么剩下的代码都不需要了。

    【讨论】:

    • “B”、“D”等本身不是标志。
    • 是的,您需要先将这些转换为标志。我的回答完全不清楚。对不起。我会更新的。
    • 写起来不容易。如果这是真的,我们都会写 C。它关乎类型安全、编译时检查、常量正确性、泛型和代码重用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-09-14
    • 1970-01-01
    • 2021-09-04
    • 2014-12-09
    • 2017-02-13
    • 2016-01-07
    • 2020-01-31
    相关资源
    最近更新 更多