我有一些或多或少像这样的代码:
#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*, ...)
我应该如何编写此代码,以便两个编译器都能做正确的事情?失败了,我应该怎么写才能使它保持清晰,快速和可维护?
答案 0 :(得分:110)
最佳版本是c++17:
stringToPrint = GetFittedStringToPrint(gfx, font, rect, stringToPrint);
然后
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
回到c++14,我们可以做这个奇怪的把戏:
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
或者,如果我们坚持使用c++11,则可以递归地解决它:
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,以便处理空包装)。
有关c++14版本正在做什么的详细说明。这是一个a俩/技巧,而您必须这样做才能有效地在C ++ 14中扩展参数包,这是在c++17中添加折叠表达式的原因之一。
最好由内而外地理解:
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 |= 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
-大多数编译器会在您创建变量且从不读取的情况下警告您。如果将其强制转换为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);
?
然后不需要其余所有代码。