布尔值对位域的优势
Advantages of boolean values to bit-fields
我工作的代码库很旧。虽然我们用 C++11 编译几乎所有东西。许多代码是多年前用 c 语言编写的。在旧领域开发新的 类 时,我总是发现自己必须在匹配旧方法和采用更现代的方法之间做出选择。
在大多数情况下,我更愿意尽可能坚持使用更现代的技术。然而,我经常看到的一种常见的旧做法是位域,我很难争论它的使用。我们传递了很多消息,在这里,很多时候,它们充满了单位值。举个例子:
class NewStructure
{
public:
const bool getValue1() const
{
return value1;
}
void setValue1(const bool input)
{
value1 = input;
}
private:
bool value1;
bool value2;
bool value3;
bool value4;
bool value5;
bool value6;
bool value7;
bool value8;
};
struct OldStructure
{
const bool getValue1() const
{
return value1;
}
void setValue1(const bool input)
{
value1 = input;
}
unsigned char value1 : 1;
unsigned char value2 : 1;
unsigned char value3 : 1;
unsigned char value4 : 1;
unsigned char value5 : 1;
unsigned char value6 : 1;
unsigned char value7 : 1;
unsigned char value8 : 1;
};
在这种情况下,新结构的大小为 8 个字节,旧结构为 1 个字节。
我添加了 "getter" 和 "setter" 来说明从用户角度来看,它们可以相同。我意识到也许您可以为下一个开发人员说明可读性,但除此之外,是否有理由避免使用位字段?我知道压缩字段会影响性能,但因为这些都是字符,所以填充规则仍然有效。
使用位域时需要考虑几件事情。这些是(重要性顺序取决于情况)
- 性能
位域操作在设置或读取时会导致性能下降(与直接类型相比)。 codegen 的一个简单示例显示了发出的额外指令:https://gcc.godbolt.org/z/DpcErN 然而,位域提供了更紧凑的数据,这对缓存更友好,并且可以完全抵消额外操作的任何缺点。了解真实性能影响的唯一方法是在真实用例中对实际应用程序进行基准测试。
- ABI 互操作性
位字段的字节顺序是实现定义的,因此两个编译器生成的相同结构的布局可能不同。
- 可用性
没有绑定到位域的引用,您也不能获取它的地址。这可能会影响代码并使其不那么清晰。
对于作为程序员的你来说,没有太大区别。但是访问整个字节的机器代码比访问单个位要多 simpler/shorter,因此使用位域会增加生成的代码。
在伪汇编语言中,您的 setter 可能会变成:
ldb input1,b ; get the new value into accumulator b
movb b,value1 ; put it into the variable
rts ; return from subroutine
但是位域就没那么容易了:
ldb input1,b ; get the new value into accumulator b
movb bitfields,a ; get current bitfield values into accumulator a
cmpb b,#0 ; See what to do.
brz clearvalue1: ; If it's zero, go to clearing the bit
orb #,a ; set the bit representing value1.
bra resume: ; skip the clearing code.
clearvalue1:
andb #f,a ; clear the bit representing value1
resume:
movb a,bitfields ; put the value back
rts ; return
而且它必须为您的 8 个成员中的每个 setter 执行此操作,对于 getter 也必须执行类似的操作。它加起来。此外,即使是今天最笨的编译器也可能会内联全字节 setter 代码,而不是实际进行子例程调用。对于位域 setter,这可能取决于您是针对速度还是 space.
进行编译优化
而您只询问了布尔值。如果它们是整数位域,那么编译器必须处理加载、屏蔽掉先前的值、移动值以对齐到它的字段、屏蔽掉未使用的位、and
/or
值到位,然后将其写回内存。
那么你为什么要使用一个而不是另一个呢?
- 位域速度较慢,但打包数据的效率更高。
- 非位域更快,并且需要更少的机器代码来访问。
作为开发者,这是您的判断。如果您要同时在内存中保留 Structure
的多个实例,那么节省内存可能是值得的。如果您不打算同时在内存中拥有该结构的许多实例,编译后的代码膨胀会抵消内存节省,并且您会牺牲速度。
template<typename enum_type,size_t n_bits>
class bit_flags{
std::bitset<n_bits> bits;
auto operator[](enum_type bit){return bits[bit];};
auto& set(enum_type bit)){return set(bit);};
auto& reset(enum_type bit)){return set(bit);};
//go on with flip et al...
static_assert(std::is_enum<enum_type>{});
};
enum class v_flags{v1,v2,/*...*/vN};
bit_flags<v_flags,v_flags::vN+1> my_flags;
my_flags.set(v_flags::v1);
my_flags.[v_flags::v2]=true;
std::bitset
与 bool
位字段一样有效。您可以将其包装在 class 中,以强制使用 enum
中定义的名称的每一位。现在您有一个小型但可扩展的实用程序,可用于多个不同的 bool
标志集。 C++17 让它更方便:
template<auto last_flag, typename enum_type=decltype(last_flag)>
class bit_flags{
std::bitset<last_flag+1> bits;
//...
};
bit_flags<v_flags::vN+1> my_flags;
我工作的代码库很旧。虽然我们用 C++11 编译几乎所有东西。许多代码是多年前用 c 语言编写的。在旧领域开发新的 类 时,我总是发现自己必须在匹配旧方法和采用更现代的方法之间做出选择。
在大多数情况下,我更愿意尽可能坚持使用更现代的技术。然而,我经常看到的一种常见的旧做法是位域,我很难争论它的使用。我们传递了很多消息,在这里,很多时候,它们充满了单位值。举个例子:
class NewStructure
{
public:
const bool getValue1() const
{
return value1;
}
void setValue1(const bool input)
{
value1 = input;
}
private:
bool value1;
bool value2;
bool value3;
bool value4;
bool value5;
bool value6;
bool value7;
bool value8;
};
struct OldStructure
{
const bool getValue1() const
{
return value1;
}
void setValue1(const bool input)
{
value1 = input;
}
unsigned char value1 : 1;
unsigned char value2 : 1;
unsigned char value3 : 1;
unsigned char value4 : 1;
unsigned char value5 : 1;
unsigned char value6 : 1;
unsigned char value7 : 1;
unsigned char value8 : 1;
};
在这种情况下,新结构的大小为 8 个字节,旧结构为 1 个字节。
我添加了 "getter" 和 "setter" 来说明从用户角度来看,它们可以相同。我意识到也许您可以为下一个开发人员说明可读性,但除此之外,是否有理由避免使用位字段?我知道压缩字段会影响性能,但因为这些都是字符,所以填充规则仍然有效。
使用位域时需要考虑几件事情。这些是(重要性顺序取决于情况)
- 性能
位域操作在设置或读取时会导致性能下降(与直接类型相比)。 codegen 的一个简单示例显示了发出的额外指令:https://gcc.godbolt.org/z/DpcErN 然而,位域提供了更紧凑的数据,这对缓存更友好,并且可以完全抵消额外操作的任何缺点。了解真实性能影响的唯一方法是在真实用例中对实际应用程序进行基准测试。
- ABI 互操作性
位字段的字节顺序是实现定义的,因此两个编译器生成的相同结构的布局可能不同。
- 可用性
没有绑定到位域的引用,您也不能获取它的地址。这可能会影响代码并使其不那么清晰。
对于作为程序员的你来说,没有太大区别。但是访问整个字节的机器代码比访问单个位要多 simpler/shorter,因此使用位域会增加生成的代码。
在伪汇编语言中,您的 setter 可能会变成:
ldb input1,b ; get the new value into accumulator b
movb b,value1 ; put it into the variable
rts ; return from subroutine
但是位域就没那么容易了:
ldb input1,b ; get the new value into accumulator b
movb bitfields,a ; get current bitfield values into accumulator a
cmpb b,#0 ; See what to do.
brz clearvalue1: ; If it's zero, go to clearing the bit
orb #,a ; set the bit representing value1.
bra resume: ; skip the clearing code.
clearvalue1:
andb #f,a ; clear the bit representing value1
resume:
movb a,bitfields ; put the value back
rts ; return
而且它必须为您的 8 个成员中的每个 setter 执行此操作,对于 getter 也必须执行类似的操作。它加起来。此外,即使是今天最笨的编译器也可能会内联全字节 setter 代码,而不是实际进行子例程调用。对于位域 setter,这可能取决于您是针对速度还是 space.
进行编译优化而您只询问了布尔值。如果它们是整数位域,那么编译器必须处理加载、屏蔽掉先前的值、移动值以对齐到它的字段、屏蔽掉未使用的位、and
/or
值到位,然后将其写回内存。
那么你为什么要使用一个而不是另一个呢?
- 位域速度较慢,但打包数据的效率更高。
- 非位域更快,并且需要更少的机器代码来访问。
作为开发者,这是您的判断。如果您要同时在内存中保留 Structure
的多个实例,那么节省内存可能是值得的。如果您不打算同时在内存中拥有该结构的许多实例,编译后的代码膨胀会抵消内存节省,并且您会牺牲速度。
template<typename enum_type,size_t n_bits>
class bit_flags{
std::bitset<n_bits> bits;
auto operator[](enum_type bit){return bits[bit];};
auto& set(enum_type bit)){return set(bit);};
auto& reset(enum_type bit)){return set(bit);};
//go on with flip et al...
static_assert(std::is_enum<enum_type>{});
};
enum class v_flags{v1,v2,/*...*/vN};
bit_flags<v_flags,v_flags::vN+1> my_flags;
my_flags.set(v_flags::v1);
my_flags.[v_flags::v2]=true;
std::bitset
与 bool
位字段一样有效。您可以将其包装在 class 中,以强制使用 enum
中定义的名称的每一位。现在您有一个小型但可扩展的实用程序,可用于多个不同的 bool
标志集。 C++17 让它更方便:
template<auto last_flag, typename enum_type=decltype(last_flag)>
class bit_flags{
std::bitset<last_flag+1> bits;
//...
};
bit_flags<v_flags::vN+1> my_flags;