何时值得使用位字段?

Rus*_*sel 48 c c++ bit-fields

是否值得使用C的位域实现?如果是的话,什么时候使用?

我正在查看一些仿真器代码,看起来芯片的寄存器没有使用位字段实现.

这是出于性能原因(或其他原因)而避免的吗?

是否仍然使用位字段?(即固件放在实际芯片上等)

Oli*_*rth 26

通常仅在需要将结构字段映射到特定位片时使用位域,其中一些硬件将解释原始位.一个例子可能是组装IP包头.我无法看到仿真器使用位域建模寄存器的令人信服的理由,因为它永远不会触及真正的硬件!

虽然位字段可以导致整洁的语法,但它们非常依赖于平台,因此不可移植.更简单但更冗长的方法是使用直接按位操作,使用移位和位掩码.

如果在某些物理接口上使用位域除了组装(或反汇编)结构之外的其他任何东西,性能可能会受到影响.这是因为每次从比特字段读取或写入时,编译器都必须生成代码来进行屏蔽和移位,这将烧掉周期.

  • 关于"燃烧周期"问题,我发现使用最小整数类型而不是使用位域确实更快.除了布尔标志(屏蔽很容易且不需要移位),我同意你的意见:) (3认同)
  • @OliCharlesworth,网络小端或大端问题将使您使用位字段传递数据包标头失败。C++ 标准也没有定义位域的存储方式,它是特定于实现的。而且基于位域的性能不好,位域就没用了。 (3认同)
  • @ZijingWu,"特定于实现"(或"平台/编译器依赖")并没有使某些东西变得无用.它只是意味着应用有限,你必须要小心. (2认同)

caf*_*caf 20

尚未提到的用于位域的一个用途是unsigned位域提供算术模数为"免费"的二次幂.例如,给定:

struct { unsigned x:10; } foo;
Run Code Online (Sandbox Code Playgroud)

算术foo.x将被执行模2 10 = 1024.

(&当然,通过使用按位运算可以直接实现相同的功能- 但有时可能会让编译器为您编写更清晰的代码).

  • @ZijingWu:是的,这就是我在最后一段中提到的. (4认同)
  • 当sizeof(unsigned)== 4时,我不会假设`sizeof(foo)== sizeof(unsigned)` (3认同)
  • 值得注意的是,这里可能是`sizeof(foo)== sizeof(unsigned)`,即你没有保存任何内存,你只需要一个更好的语法. (2认同)

Ton*_*roy 9

FWIW,只关注相对表现问题 - 一个bodgy基准:

#include <time.h>
#include <iostream>

struct A
{
    void a(unsigned n) { a_ = n; }
    void b(unsigned n) { b_ = n; }
    void c(unsigned n) { c_ = n; }
    void d(unsigned n) { d_ = n; }
    unsigned a() { return a_; }
    unsigned b() { return b_; }
    unsigned c() { return c_; }
    unsigned d() { return d_; }
    volatile unsigned a_:1,
                      b_:5,
                      c_:2,
                      d_:8;
};

struct B
{
    void a(unsigned n) { a_ = n; }
    void b(unsigned n) { b_ = n; }
    void c(unsigned n) { c_ = n; }
    void d(unsigned n) { d_ = n; }
    unsigned a() { return a_; }
    unsigned b() { return b_; }
    unsigned c() { return c_; }
    unsigned d() { return d_; }
    volatile unsigned a_, b_, c_, d_;
};

struct C
{
    void a(unsigned n) { x_ &= ~0x01; x_ |= n; }
    void b(unsigned n) { x_ &= ~0x3E; x_ |= n << 1; }
    void c(unsigned n) { x_ &= ~0xC0; x_ |= n << 6; }
    void d(unsigned n) { x_ &= ~0xFF00; x_ |= n << 8; }
    unsigned a() const { return x_ & 0x01; }
    unsigned b() const { return (x_ & 0x3E) >> 1; }
    unsigned c() const { return (x_ & 0xC0) >> 6; }
    unsigned d() const { return (x_ & 0xFF00) >> 8; }
    volatile unsigned x_;
};

struct Timer
{
    Timer() { get(&start_tp); }
    double elapsed() const {
        struct timespec end_tp;
        get(&end_tp);
        return (end_tp.tv_sec - start_tp.tv_sec) +
               (1E-9 * end_tp.tv_nsec - 1E-9 * start_tp.tv_nsec);
    }
  private:
    static void get(struct timespec* p_tp) {
        if (clock_gettime(CLOCK_REALTIME, p_tp) != 0)
        {
            std::cerr << "clock_gettime() error\n";
            exit(EXIT_FAILURE);
        }
    }
    struct timespec start_tp;
};

template <typename T>
unsigned f()
{
    int n = 0;
    Timer timer;
    T t;
    for (int i = 0; i < 10000000; ++i)
    {
        t.a(i & 0x01);
        t.b(i & 0x1F);
        t.c(i & 0x03);
        t.d(i & 0xFF);
        n += t.a() + t.b() + t.c() + t.d();
    }
    std::cout << timer.elapsed() << '\n';
    return n;
}

int main()
{
    std::cout << "bitfields: " << f<A>() << '\n';
    std::cout << "separate ints: " << f<B>() << '\n';
    std::cout << "explicit and/or/shift: " << f<C>() << '\n';
}
Run Code Online (Sandbox Code Playgroud)

我的测试机器上的输出(运行时数量变化约20%):

bitfields: 0.140586
1449991808
separate ints: 0.039374
1449991808
explicit and/or/shift: 0.252723
1449991808
Run Code Online (Sandbox Code Playgroud)

建议在最近的Athlon上使用g ++ -O3,位域比单独的整数慢一些,并且这个特殊的和/或/ bitshift实现的性能至少要差两倍(比其他操作如内存读取更差)上面的波动性强调了写入,并且存在循环开销等,因此结果中的差异被低估了).

如果您处理数百兆字节的结构,主要是位域或主要是不同的内容,缓存问题可能会成为主导 - 所以在您的系统中进行基准测试.


更新:user2188211尝试编辑被拒绝,但有用地说明了随着数据量的增加,位域如何变得更快:"在上述代码的[修改版本]中迭代几百万个元素的向量时,这样变量就可以了不驻留在缓存或寄存器中,位域代码可能是最快的."

template <typename T>
unsigned f()
{
    int n = 0;
    Timer timer;
    std::vector<T> ts(1024 * 1024 * 16);
    for (size_t i = 0, idx = 0; i < 10000000; ++i)
    {
        T& t = ts[idx];
        t.a(i & 0x01);
        t.b(i & 0x1F);
        t.c(i & 0x03);
        t.d(i & 0xFF);
        n += t.a() + t.b() + t.c() + t.d();
        idx++;
        if (idx >= ts.size()) {
            idx = 0;
        }
    }
    std::cout << timer.elapsed() << '\n';
    return n;
}
Run Code Online (Sandbox Code Playgroud)

来自示例运行的结果(g ++ -03,Core2Duo):

 0.19016
 bitfields: 1449991808
 0.342756
 separate ints: 1449991808
 0.215243
 explicit and/or/shift: 1449991808
Run Code Online (Sandbox Code Playgroud)

当然,时间的所有相对性以及实现这些字段的方式在您的系统环境中可能并不重要.


ues*_*esp 7

我在两种情况下看到/使用了位字段:计算机游戏和硬件接口.硬件使用非常简单:硬件需要某种位格式的数据,您可以手动定义或通过预定义的库结构定义.它取决于特定的库,它们是使用位字段还是只是位操作.

在"旧时代"计算机游戏中经常使用位字段来尽可能地充分利用计算机/磁盘存储器.例如,对于RPG中的NPC定义,您可能会找到(编写示例):

struct charinfo_t
{
     unsigned int Strength : 7;  // 0-100
     unsigned int Agility : 7;  
     unsigned int Endurance: 7;  
     unsigned int Speed : 7;  
     unsigned int Charisma : 7;  
     unsigned int HitPoints : 10;    //0-1000
     unsigned int MaxHitPoints : 10;  
     //etc...
};
Run Code Online (Sandbox Code Playgroud)

你不会在更现代的游戏/软件中看到这么多,因为随着计算机获得更多内存,节省的空间也越来越差.当你的计算机只有16MB时,节省1MB的内存是一个大问题,但是当你有4GB时则不是这么多.

  • 如今计算机可能拥有更多RAM,但保持较低的内存使用率可以帮助将其保留在CPU内存缓存中,从而提高性能.另一方面,位域需要更多指令才能访问它们,这会降低性能.哪个更重要? (5认同)

AnT*_*AnT 7

位域的主要目的是通过实现更紧密的数据打包,提供一种在大规模实例化聚合数据结构中节省内存的方法。

整个想法是利用某些结构类型中有多个字段的情况,这些字段不需要某些标准数据类型的整个宽度(和范围)。这为您提供了将多个此类字段打包到一个分配单元中的机会,从而减少了结构类型的整体大小。极端的例子是布尔字段,它可以由单个位表示(例如,其中的 32 个可以打包到单个unsigned int分配单元中)。

显然,这仅在减少内存消耗的优点超过对存储在位字段中的值的较慢访问的缺点的情况下才有意义。然而,这种情况经常出现,这使得位域成为绝对不可缺少的语言特性。这应该可以回答您关于位域现代使用的问题:它们不仅被使用,而且在面向处理大量同构数据(例如大图)的任何实际有意义的代码中都是必不可少的,因为它们的内存- 节省的好处大大超过了任何个人访问性能的损失。

在某种程度上,位域的用途与诸如“小”算术类型之类的东西非常相似:signed/unsigned char, short, float。在实际的数据处理代码中,通常不会使用任何小于intor 的类型double(除了少数例外)。算术类型喜欢signed/unsigned charshortfloat存在只是充当“存储”的类型:为存储器节省结构类型的紧凑成员在他们的范围(或精度)已知是足够的情况。位域只是朝着同一方向迈出的又一步,它以更高的性能换取更大的内存节省优势。

因此,这为我们提供了一组相当清晰的条件,在这些条件下,使用位域是值得的:

  1. 结构类型包含多个字段,这些字段可以打包成更少的位。
  2. 该程序实例化了大量该结构类型的对象。

如果满足条件,则连续声明所有位可打包字段(通常在结构类型的末尾),为它们分配适当的位宽(并且通常采取一些步骤来确保位宽合适) . 在大多数情况下,对这些字段进行排序以实现最佳打包和/或性能是有意义的。


位域还有一个奇怪的二次使用:使用它们来映射各种外部指定的表示中的位组,如硬件寄存器、浮点格式、文件格式等。 这从来不是位域的正确使用,即使出于某种无法解释的原因,这种位域滥用继续在现实生活中的代码中弹出。不要这样做。