如何在C ++中编写可维护的,快速的编译时位掩码?

时间:2019-02-20 12:20:37

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

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

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

我应该如何编写此代码,以便两个编译器都能做正确的事情?失败了,我应该怎么写才能使它保持清晰,快速和可维护?

6 个答案:

答案 0 :(得分:110)

最佳版本是

stringToPrint = GetFittedStringToPrint(gfx, font, rect, stringToPrint);

然后

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

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

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

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


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

最好由内而外地理解:

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

这只是将 r |= (1ull << indexes) // side effect, used r更新为固定索引。 1<<indexes是一个参数包,因此我们必须对其进行扩展。

剩余的工作是提供一个参数包,以在其中展开indexes

迈出一步:

indexes

在这里,我们将表达式转换为(void( r |= (1ull << indexes) // side effect, used ),0) ,表示我们不关心它的返回值(我们只希望设置void的副作用-在C ++中,像{{1}这样的表达式}还会返回将r设置为的值。)

然后,我们使用逗号运算符a |= ba丢弃,“值”,并返回值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 -大多数编译器会在您创建变量且从不读取的情况下警告您。如果将其强制转换为discard,所有编译器都不会抱怨,这是一种表示“是的,我知道,我没有使用它”的方式,因此它可以消除警告。

答案 1 :(得分:46)

您正在寻找的优化似乎是循环剥皮,该循环剥皮在-O3上启用,或者通过-fpeel-loops手动启用。我不确定为什么它属于循环剥离而不是循环展开的范围,但是可能不愿意展开其中具有非本地控制流的循环(可能存在范围检查)。

但是,默认情况下,GCC停止了剥离所有迭代的工作,这显然是必要的。实验上,传递-O2 -fpeel-loops --param max-peeled-insns=200(默认值为100)可以使用原始代码https://godbolt.org/z/NNWrga

完成该工作。

答案 2 :(得分: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's,答案已经很好地涵盖了这一点。

答案 3 :(得分: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 godbolt处平凡地生成一条-O1指令:

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

答案 4 :(得分: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函数相比,此方法的优势在于,由于rule of Chiel,它的编译速度可能会稍快。

答案 5 :(得分:1)

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

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

比写起来容易得多

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

然后不需要其余所有代码。