何时值得使用位字段?

时间:2010-11-21 23:11:48

标签: c++ c bit-fields

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

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

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

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

11 个答案:

答案 0 :(得分:24)

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

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

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

答案 1 :(得分:19)

尚未提及的位域的一个用途是unsigned位域提供算术模数为“免费”的2的幂。例如,给定:

struct { unsigned x:10; } foo;

foo.x上的算术将以2 10 = 1024进行模拟。

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

答案 2 :(得分: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';
}

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

bitfields: 0.140586
1449991808
separate ints: 0.039374
1449991808
explicit and/or/shift: 0.252723
1449991808

建议在最近的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;
}

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

 0.19016
 bitfields: 1449991808
 0.342756
 separate ints: 1449991808
 0.215243
 explicit and/or/shift: 1449991808

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

答案 3 :(得分: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...
};

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

答案 4 :(得分:3)

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

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

显然,这只有在减少内存消耗的优点超过对存储在位字段中的值的较慢访问权限的情况下才有意义。然而,这种情况经常出现,这使得位域成为绝对不可或缺的语言特征。这应该回答你关于比特字段的现代使用的问题:不仅使用它们,它们在任何具有实际意义的代码中都是强制性的,这些代码面向处理大量同类数据(例如大图,例如),因为它们的记忆 - 节省的好处大大超过任何个人访问性能的惩罚。

在某种程度上,其目的中的位域非常类似于&#34; small&#34;算术类型:signed/unsigned charshortfloat。在实际的数据处理代码中,通常不会使用小于intdouble的任何类型(少数例外)。像signed/unsigned charshortfloat这样的算术类型只是用作&#34;存储&#34; types:作为已知范围(或精度)已足够的结构类型的内存节省紧凑成员。位域只是朝着同一方向迈出的又一步,它可以提供更高的性能,从而大大节省内存。

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

  1. 结构类型包含多个字段,可以打包成较少的位。
  2. 程序实例化该结构类型的大量对象。
  3. 如果满足条件,则连续声明所有可包装的字段(通常在结构类型的末尾),为它们分配适当的位宽(通常,采取一些步骤以确保位宽是合适的)。在大多数情况下,有必要对这些字段的排序进行处理,以实现最佳的打包和/或性能。

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

答案 5 :(得分:2)

旧日中使用位字段来保存程序内存。

它们会降低性能,因为寄存器无法使用它们,因此必须将它们转换为整数才能对它们执行任何操作。它们倾向于导致更复杂的代码,这些代码不可移植且难以理解(因为您必须始终屏蔽和取消屏蔽事物以实际使用这些值。)

查看http://www.nethack.org/的来源,了解其所有位域荣耀的前提信息!

答案 6 :(得分:1)

用于比特字段的一种用途是用于在编写嵌入代码时镜像硬件寄存器。但是,由于位顺序与平台有关,因此如果硬件命令其位与处理器不同,则它们不起作用。也就是说,我不能再考虑使用位域了。你最好实现一个可以跨平台移植的位操作库。

答案 7 :(得分:1)

在现代代码中,使用位域的原因只有一个:在结构/类中控制boolenum类型的空间要求。例如(C ++):

enum token_code { TK_a, TK_b, TK_c, ... /* less than 255 codes */ };
struct token {
    token_code code      : 8;
    bool number_unsigned : 1;
    bool is_keyword      : 1;
    /* etc */
};

IMO基本上没有理由不为:1使用bool位域,因为现代编译器会为它生成非常有效的代码。但是,在C中,请确保bool typedef是C99 _Bool或者 unsigned int失败,因为签名的1位字段只能包含值{ {1}}和0(除非你以某种方式拥有非二进制补码机器)。

使用枚举类型时,始终使用与原始整数类型之一(普通CPU上的8/16/32/64位)的大小相对应的大小,以避免生成低效代码(重复读取 - 修改 - 写入循环) ,通常)。

通常建议使用位域来排列具有一些外部定义的数据格式(包头,内存映射的I / O寄存器)的结构,但实际上我认为这是一种不好的做法,因为C不能给你足够的控制字节顺序,填充和(对于I / O寄存器)确切地说发出了哪些汇编序列。如果你想看看这个区域缺少多少C,请看看Ada的代表条款。

答案 8 :(得分:1)

在70年代,我使用位字段来控制trs80上的硬件。显示器/键盘/盒式磁带/磁盘都是内存映射设备。各个位控制着各种各样的东西。

  1. 受控制的32列与64列显示。
  2. 同一存储单元中的位0是盒式串行数据输入/输出。
  3. 我记得,磁盘驱动器控件有很多。总共有4个字节。我认为有一个2位驱动器选择。但是很久以前。那时令人印象深刻的是,至少有两种不同的c编译器用于植物形态。

    另一个观察是位字段确实是特定于平台的。不期望具有位字段的程序应该移植到另一个平台。

答案 9 :(得分:0)

Boost.Thread至少在Windows shared_mutex中使用位域:

    struct state_data
    {
        unsigned shared_count:11,
        shared_waiting:11,
        exclusive:1,
        upgrade:1,
        exclusive_waiting:7,
        exclusive_waiting_blocked:1;
    };

答案 10 :(得分:-3)

考虑的另一种方法是使用虚拟结构(从未实例化)指定位字段结构,其中每个字节代表一位:

struct Bf_format
{
  char field1[5];
  char field2[9];
  char field3[18];
};

使用此方法 sizeof 给出位域的宽度, offsetof 给出位域的偏移量。至少在GNU gcc的情况下,逐位运算(使用常量移位和掩码)的编译器优化似乎已经与(基本语言)位字段进行了粗略的奇偶校验。

我编写了一个C ++头文件(使用这种方法),它允许以高性能,更便携,更灵活的方式定义和使用位域结构:https://github.com/wkaras/C-plus-plus-library-bit-fields。因此,除非您使用C语言,否则我认为很少有充分的理由将基本语言工具用于位字段。