When Is It Worthwhile to Use Bit Fields

When to use bit-fields in C

Now I am curious, [are flags] the only way bitfields are used practically?

No, flags are not the only way bitfields are used. They can also be used to store values larger than one bit, although flags are more common. For instance:

typedef enum {
NORTH = 0,
EAST = 1,
SOUTH = 2,
WEST = 3
} directionValues;

struct {
unsigned int alice_dir : 2;
unsigned int bob_dir : 2;
} directions;

Do we need to use bitfields to save space?

Bitfields do save space. They also allow an easier way to set values that aren't byte-aligned. Rather than bit-shifting and using bitwise operations, we can use the same syntax as setting fields in a struct. This improves readability. With a bitfield, you could write

directions.alice_dir = WEST;
directions.bob_dir = SOUTH;

However, to store multiple independent values in the space of one int (or other type) without bitfields, you would need to write something like:

#define ALICE_OFFSET 0
#define BOB_OFFSET 2
directions &= ~(3<<ALICE_OFFSET); // clear Alice's bits
directions |= WEST<<ALICE_OFFSET; // set Alice's bits to WEST
directions &= ~(3<<BOB_OFFSET); // clear Bob's bits
directions |= SOUTH<<BOB_OFFSET; // set Bob's bits to SOUTH

The improved readability of bitfields is arguably more important than saving a few bytes here and there.

Why do we use int? How much space is occupied?

The space of an entire int is occupied. We use int because in many cases, it doesn't really matter. If, for a single value, you use 4 bytes instead of 1 or 2, your user probably won't notice. For some platforms, size does matter more, and you can use other data types which take up less space (char, short, uint8_t, etc.).

As I understand only 1 bit is occupied in memory, but not the whole unsigned int value. Is it correct?

No, that is not correct. The entire unsigned int will exist, even if you're only using 8 of its bits.

When and why would you use Bitfields in code?

Some uses that immediately come to mind are:

  • implementing communications protocols;
  • storing user data in objects where you have limited space;
  • extending data structures in existing protocols (similar to the above);
  • performing multiple tests in a single operation;

Advantages of boolean values to bit-fields

There are several things to consider when using bitfields. Those are (order of importance would depend on situation)

  • Performance

Bitfields operation incur performance penalty when set or read (as compared to direct types). A simple example of codegen shows the extra instructions emitted: https://gcc.godbolt.org/z/DpcErN However, bitfields provide for more compact data, which becomes more cache-friendly, and that could completely outweigh any drawbacks of additional operations. The only way to understand the real performance impact is by benchmarking actual application in the real use case.

  • ABI Interoperability

Endiannes of bit fields is implementation defined, so layout of the same struct produced by two compiler can differ.

  • Usability

There is no reference binding to a bitfield, nor you can take it's address. This could affect code and make it less clear.

Bit manipulations good practices

The problem with bit fields is that the C standard does not dictate that the order in which they are defined is the same as the order that they are implemented. So you may not be setting the bits you think you are.

Section 6.7.2.1p11 of the C standard states:

An implementation may allocate any addressable storage unit large
enough to hold a bit- field. If enough space remains, a bit-field
that immediately follows another bit-field in a structure shall be
packed into adjacent bits of the same unit. If insufficient space
remains, whether a bit-field that does not fit is put into
the next unit or overlaps adjacent units is
implementation-defined. The order of allocation of bit-fields within
a unit (high-order to low-order or low-order to high-order) is
implementation-defined.
The alignment of the addressable storage
unit is unspecified.

As an example, look at the definition of struct iphdr, which represents an IP header, from the /usr/include/netinet/ip.h file file on Linux:

struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ihl:4;
unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
unsigned int version:4;
unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
u_int8_t tos;
...

You can see here that the bitfields are placed in a different order depending on the implementation. You also shouldn't use this specific check because this behavior is system dependent. It is acceptable for this file because it is part of the system. Other systems may implement this in different ways.

So don't use a bitfield.

The best way to do this is to set the required bits. However, it would make sense to define named constants for each bit and to perform a bitwise OR of the constants you want to set. For example:

const uint8_t BIT_BYTE =     0x1;
const uint8_t BIT_HW = 0x2;
const uint8_t BIT_WORD = 0x4;
const uint8_t BIT_GO = 0x8;
const uint8_t BIT_I_EN = 0x10;
const uint8_t BIT_REEN = 0x20;
const uint8_t BIT_WEEN = 0x40;
const uint8_t BIT_LEEN = 0x80;

DMA_base_ptr[DMA_CONTROL_OFFS] = BIT_LEEN | BIT_GO | BIT_WORD;

Which end of a bit field is the most significant bit?

C99 standard 6.7.2.1/10 (emphasis mine):

An implementation may allocate any addressable storage unit large enough to hold a bitfield. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.

So, the order must be documented by your compiler implementation.

However, so much about how bitfields are implemented are implementation defined or unspecified that using them to model hardware, wire-protocol, or file format bit fields in a portable fashion is not worth the trouble to attempt.

If you want your 'bit fields' to model something external to your program (like the above things), use explicit masks, setting and clearing the bits using the standard bit-wise operators (|, '&,~,<<`, etc.). Use helper inline functions (or even macros if you must) to make this easier/clearer in your code.



Related Topics



Leave a reply



Submit