如何在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
枚举具有位的位置,而不是已经扩展的位,所以我可以做(1ULL << B) | ... | (1ULL << O)
亚历克斯·雷肯

3
不利的一面是,实际的名称很长且不规则,要掩盖所有线路噪声,要掩盖哪些标志并不是一件容易的事。
Alex Reinking

4
@AlexReinking您可以做到(1ULL << Constant)| 每行,并在不同行上对齐常量名称,这在眼睛上会更容易。
einpoklum

我想在这里与缺乏使用无符号类型的问题,GCC一直在移位这里的符号/无符号hybrid.Result静态丢弃校正溢出和类型转换的麻烦是int位操作的结果可能是int,也可以是 long long根据不同的值并且在形式上enum不等于int常量。lang叫“好像”,海湾合作委员会保持学究
迅速-星期五派

Answers:


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

Godbolt与所有这3个 -您可以切换CPP_VERSION定义,并获得相同的装配。

在实践中,我会尽量使用最现代的。14击败11,因为我们没有递归,因此符号长度为O(n ^ 2)(这可能会爆炸编译时间和编译器内存使用情况);17比14好,因为编译器不必通过死代码消除该数组,并且该数组技巧很丑陋。

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


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

最好由内而外的理解:

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

这只是更新r1<<indexes一个固定的指标。 indexes是一个参数包,因此我们必须对其进行扩展。

剩下的工作是提供一个参数包以在其中扩展indexes

迈出一步:

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

在这里,我们将表达式转换为void,表示我们不在乎其返回值(我们只希望set 的副作用r-在C ++中,类似的表达式a |= b也返回它们设置的值a为)。

然后,我们使用逗号运算符,0丢弃void“值”,然后返回value 0。因此,这是一个表达式,其值为0和作为计算的副作用,0它在中设置了一位r

  int discard[] = {0,(void(
    r |= (1ull << indexes) // side effect, used
  ),0)...};

此时,我们扩展参数pack 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,这也将bit设置r为副作用。然后,我们将{}数组构造指令分配给数组discard

接下来,我们转换discardvoid-如果您创建变量并且从不读取,大多数编译器都会警告您。如果将其强制转换为void,所有编译器都不会抱怨,这是一种表示“是的,我知道,我没有使用它”的方式,因此它可以消除警告。


38
抱歉,但是C ++ 14代码是什么。我不知道
詹姆斯,

14
@James这是一个很好的示例,说明了为什么非常欢迎C ++ 17中的fold表达式。事实证明,它和类似的技巧是在没有任何递归的情况下“就地”扩展包的有效方法,并且编译器发现易于优化。
Yakk-Adam Nevraumont

4
@ruben多行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 '19

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
很烂

@ Yakk-AdamNevraumont:您确实意识到我发布了两个版本的代码,第一个版本从C ++ 14开始,第二个版本专门针对C ++ 11?(以考虑其局限性)
Matthieu M.

1
最好使用std :: underlying_type而不是std :: uint64_t。
詹姆斯,

@詹姆斯:其实没有。请注意,EnumSet<E>它不会E直接使用as值,而是使用1 << e。这是一个不同的域完全,这实际上是什么使这个类如此宝贵=>没有不慎被索引的机会e,而不是1 << e
Matthieu M.19年

@MatthieuM。你是对的。我把它与我们自己的实现相混淆,后者与您的实现非常相似。使用(1 << e)的缺点是,如果e在underlying_type的大小范围内,则可能是UB,可能是编译器错误。
詹姆斯,

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

链接到编译器资源管理器: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”等本身不是标志。
01MichałŁoś

是的,您需要先将它们转换为标志。我的回答根本不清楚。抱歉。我会更新。
ANone
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.