10 ms·
It neglects to mention that bit fields have always been the buggiest part of C compilers, and there is never a good enough reason to rely on them, if you have a
by ncmncm 5y ago
It neglects to mention that bit fields have always been the buggiest part of C compilers, and there is never a good enough reason to rely on them, if you have a choice at all. Honest shift-and-mask operations on unsigned machine words are always better, if you absolutely must pack bitwise.
- chrisseaton 5y ago> shift-and-mask operations on unsigned machine words are always better ... but that's what a bit field is?
- layer8 5y agoBit fields are a specific C language feature, allowing to treat bit slices as a unit whose length doesn’t need to correspond to one of the integer types. See for example https://docs.microsoft.com/en-us/cpp/c-language/c-bit-fields?view=msvc-170 https://docs.microsoft.com/en-us/cpp/c-language/c-bit-fields....
- chrisseaton 5y agoYeah they compile to the same machine code operations though. If those machine code operations aren't right as a bitfield then they aren't going to be right done manually either. https://godbolt.org/z/csvTx89EG https://godbolt.org/z/csvTx89EG
- layer8 5y agoBit field support being buggy exactly means that they don’t compile to the same machine code as the bit shifting/masking code you would write by hand (if your hand-written code is correct).
- minipci1321 5y ago> Yeah they compile to the same machine code operations though. Not always. Switch your example to AARCH64 and check out the BFI instruction.
- alcover 5y agoIs it not rather void bar(struct y *s, unsigned int foo) { s->c = (s->c & 0xf0) | foo; }
- tom_ 5y agoARM is little-endian, and by tradition bitfield bit indexes are assigned from least significant (bit 0 in ARM terms) to more significant. b occupies bits 4-7 inclusive.
- jcranmer 5y agoThat's not what a bit field is. A bit field (in C/C++) is a weird object type that can only exist in a structure or union type, which kind of acts like an underlying regular integral type except for those situations where it does not. For an example of why compilers might have issues compiling bit fields properly (although this requires C++, since C's ternary operator works on rvalues, not lvalues): struct A { int x: 3; int y: 5 } a; (choice ? a.x : a.y) = val; Enjoy making that codegen work properly.
- mwint 5y agoCan someone explain further why this is so hard? I’m not familiar enough with any of this to understand without some help.
- ncmncm 5y agoIt is just very finicky, with myriad edge cases easy to get wrong, and even easier to neglect to have complete tests for. Each target CPU design and version has quirks. Many involve sign extension.
- jstimpfle 5y agoExtracting a subrange of bits and shifting them to the beginning is not exactly rocket science.
- ncmncm 5y agoExperience indicates otherwise. If you must use bit fields, make them unsigned. Bugs love to hide under signed bit fields.
- jstimpfle 5y agoThat makes sense, looking at how signed arithmetic works on different architectures it would feel strange to use signed bitfields. Unsigned bitfields are a nice way to get modular arithmetic with n bits without syntactic clutter.
- jmwilson 5y agoWith shifts and masks you know where the bits are. With bitfields, you don't because the specification leaves everything up to the compiler. struct foo { char a : 4; char b : 4; }; Is a in the high-order 4 bits, or the lower 4 bits? Both choices are allowed, so it's up to the compiler and makes the code non-portable.
- deleted 5y ago[deleted]
- dmitrygr 5y agosometimes you do not care, and x = foo.a is simpler than x = (foo & FOO_MASK_A) >> FOO_SHIFT_A and for assignments, the difference is even bigger: foo.a = x is much better than foo = (foo &~ FOO_MASK_A) | ((a << FOO_SHIFT_A) & FOO_MASK_A)
- zozbot234 5y agoIf "foo" is defined as part of an API/ABI that's used in multiple compile units you will always care, since otherwise a random change in "implementation defined" bitfield encodings on some obscure architecture might break your build. Bitfields are a misfeature in most real-world cases.
- InitialLastName 5y agoThe case where a) you don't care about the in-memory representation of your struct and b) you care a lot about being able to pack into the absolute minimum memory space, but not enough to make sure the compiler actually packs the fields (depending on architecture and optimization settings, they might not!) is vanishingly small. The more frequent perceived use for bit-fields (in the situation where they actually work) is to pack into a serialized data format, such that memory or a data stream can be accessed elsewhere. In that case, "the compiler can do whatever it wants with your data packing" is pretty useless, since your "elsewhere" might have a different compiler that does a totally different thing.
- 5y ago
- ncmncm 5y agoPhysically, yes. The difference is whether you let the compiler generate and hide the shift-and-mask ops, or code them by hand. Normally it is better to leave details to the compiler. This is the exception to that rule. A result of people avoiding declaring bit fields in serious use cases has been that compiler vendors didn't worry too much about bitfield codegen bugs. Probably Gcc and Clang are OK on x86, by now. But that does not carry to, e.g., obscure microcontrollers. Heaven help you if your bit field members are supposed to correspond to hardware register sub-fields.
- iainmerrick 5y agoThe same applies to structs in general, not just bitfields.
- ncmncm 5y agoCommon experience is that compilers, ABIs, and instruction implementations get ordinary struct fields right.
- dmitrygr 5y ago> Honest shift-and-mask operations on unsigned machine words are always better, if you absolutely must pack bitwise Now you go ahead and teach GCC to use the arm UBFX instruction for those cases. It DOES use it for actual bitfields. shift + mask = 2-3 instructions (immediate load may be needed). UBFX is one.
- ncmncm 5y agoThe more complicated the instruction is, the less likely it was implemented to spec on all the various products and mask steppings you might execute on ... and the less likely its published definition exactly matches C or C++ Standard and platform C ABI specs. And, the less likely that ABI spec nails down all the details. Compiler implementors don't like to guess, but don't get a choice. If the instruction provided doesn't match the Standard, which do they implement? Both choices are wrong.
- mjevans 5y agoUntil I read this article that's what I thought C Bitfields _were_. I didn't realize the specification was so uselessly sloppy about alignment packing that a programmer couldn't reliably address specific bit field members with just a lowest bit first to highest bit field of exact widths. It's quite annoying that such is not what those are.
- scatters 5y agoIf the specification is lax, it's because the expected behavior is different across platforms. C (and C++) has to support platforms where bytes are more than 8 bits, where floats are non-IEEE, and (until recently) where signed integers are not 2's complement. If you want a specific behavior and don't care about portability all you have to do is read the ABI spec alongside the standard.
- ncmncm 5y agoYou have wholly missed the point of the warning. It does not matter what the spec says when the instructions fail to implement it, sometimes, on machines other than yours.
- froh 5y agoPlease add endianness (little, big and mixed) to the list, relevant to but field packing, while IEEE floats should not interfere with bit fields.
- Asooka 5y agoThe ABI for a specific platform will define how bit fields are arranged in memory, or at least you can always rely on GCC to not change behaviour. They work perfectly well for data in memory, at work we use them all the time for storing tiny integers or flags.
- ncmncm 5y agoThey work on the machines where you have tested them. The ABI defines what is supposed to happen, not what will happen. Gcc will get it right on very heavily used archs, and very heavily used archs will generally execute the instructions right. If your code only ever runs on the physical machines where you test, or only on very mainstream chip designs, then fine. You have been warned. Now it is on you.
- AdamH12113 5y agoBitfields make for much more readable code when accessing individual fields of hardware registers, although there are some caveats if the registers are poorly-designed. The main one is that bitfield writes are usually read-modify-writes, so if reading the register or writing back its current value causes something to happen, bitfields are a no-go. But when they work, you get code like: old_divider = SpiRegs.CONFIG_REG.bit.CLK_DIVIDER; SpiRegs.CONFIG_REG.bit.CLK_DIVIDER = new_divider; instead of: old_divider = (SpiRegs.CONFIG_REG & SPI_CONFIG_CLK_DIVIDER_MASK) >> SPI_CONFIG_CLK_DIVIDER_POS; SpiRegs.CONFIG_REG = (SpiRegs.CONFIG_REG & ~SPI_CONFIG_CLK_DIVIDER_MASK) | (new_divider << SPI_CONFIG_CLK_DIVIDER_POS); or the slightly nicer but even longer: config = SpiRegs.CONFIG_REG; old_divider = (config & SPI_CONFIG_CLK_DIVIDER_MASK) >> SPI_CONFIG_CLK_DIVIDER_POS; config &= ~SPI_CONFIG_CLK_DIVIDER_MASK; config |= new_divider << SPI_CONFIG_CLK_DIVIDER_POS; SpiRegs.CONFIG_REG = config; For anything other than hardware registers, I agree that they're not portable enough to rely on.
- pavon 5y agoHelper functions or macros are just as clean as the bitfield syntax. That said, hardware register access is one of those things that is intrinsically tied to a specific platform (and if you target multiple platforms, it will already be behind an abstraction layer), so you can usually know the quirks of how the toolchain for that platform supports bitfields, and use them accordingly. Still more work for people reading the code, though since there are a lot of hidden assumptions behind that deceptively simple "=" than with an explicit mask and shift.
- AdamH12113 5y ago>Helper functions or macros are just as clean as the bitfield syntax. They can be, if done right, but then I have to remember the names of all the helper functions and macros. :-) An IDE can auto-complete bitfield names. >Still more work for people reading the code, though since there are a lot of hidden assumptions behind that deceptively simple "=" than with an explicit mask and shift. Depends on the platform. IIRC on ARM a bitfield access is masking and shifting, only done by the compiler instead of me. With optimized code I often have to look at the disassembly anyway if I want to know what's really going on.
- ithinkso 5y agoBitfields are used a lot when you have constrained resources, in my case in LTE/5G both on modem and bts sides. Every struct's field takes as much as it needs to and you leave rest as 'reservedX'. You never know when a new feature will have to be implemented and few more bits will be needed for some new field. Without bitfields the code would be absolutely filled with bit-access macros decreasing readability and screwing with IDE's indexers and static analyzers big time Not to mention the pain it would be to refactor/reorder/change fields sizes which is relatively painless with bitfields
- zozbot234 5y agoThe drawback though is that you can't reference individual fields by pointer. You basically need to write the equivalents of OOP getters and setters to really keep the code tidy. The compiler can't plan for such things on its own, not across multiple compilation units at any rate.
- ithinkso 5y agoThis is a valid and a main drawback of bitfields but in this context bit-access macros would be even worse since you would have pointers to a random uint32 within a struct and good luck keeping track where's what
- ncmncm 5y agoThat is not, in fact, the main drawback of bitfields. The main drawback of bitfields is that they work in your tests and fail in the field.
- ithinkso 5y agoCould you share the details (and compilers) of the issues you had because I'm actually kinda curious. From my experience most of the bugs were just a 'normal' bugs i.e. human errors when writing and those were fixable by just figuring out what was implemented incorrectly. About 2% on a bts side were cache coherency issues because we had a multicore system without hardware coherency, so imagine, and similarily 2% were hardware issues on a modem side due to race conditions or whatever - harder to fix so workarounds. But miscompilation? x86 host tests are used to separate wheat from the chaff but the only tests anyone cares about, before commit, are on target On modem side I remember one, maybe two if you push it, issues with miscompilation but it wasn't at all related to bitfields but the compiler was doing some stupid shit with register allocations
- WalterBright 5y ago> bit fields have always been the buggiest part of C compiler Not in my experience. The buggiest part was the preprocessor. You don't hear much about preprocessor bugs anymore because the C standard doesn't dare change it, and in 40 years people have finally got them working right :-/ Personally, I had to scrap and rewrite the C preprocessor 3 times to get it right.
- ncmncm 5y agoC preprocessors used to be a big trouble spot. Now that we have good test suites, C preprocessor bugs have faded.
- camgunz 5y agoSuper agree. I read up on bit fields [0] a while ago and some of the details about them are bonkers: > Multiple adjacent bit-fields are usually packed together (although this behavior is implementation-defined) > The special unnamed bit-field of size zero can be forced to break up padding. > int b:3; may have the range of values 0..7 or -4..3 in C > on some platforms, bit-fields are packed left-to-right, on others right-to-left I wouldn't touch them unless I absolutely had to, and knew I could guarantee compiler and platform. [0]: https://en.cppreference.com/w/cpp/language/bit_field https://en.cppreference.com/w/cpp/language/bit_field
- deaddodo 5y agoThey're pretty safe on the two most popular ISAs: aarch64 and amd64.
- ncmncm 5y agoSafe enough to make people complacent, anyway.
- alternatetwo 5y agoEven then, msvc and gcc still generate them differently ...
- bsder 5y agoQuite true. C bitpacking is lousy. The best "bitpacking" I have ever dealt with is the "Erlang Bit Syntax". I really wish more languages would adopt it. See: https://www.erlang.org/doc/programming_examples/bit_syntax.html https://www.erlang.org/doc/programming_examples/bit_syntax.h...
- deaddodo 5y agoI've always thought Ada's syntax was the clearest as to what's being defined and accessed[1]. That being said, it's Ada...so good luck finding a use case. 1 - https://stackoverflow.com/questions/58493193/ada-how-to-explicitly-pack-a-bit-field-record-type https://stackoverflow.com/questions/58493193/ada-how-to-expl...
- pyjarrett 5y agoAda is often used in microcontrollers where this is very common. I write command line tools in Ada, and some other things, I've found this bit layout very useful when reading/writing binary file formats, or when binding to C and matching C struct layout.
- deaddodo 5y agoFair. I’m quite fond of Ada because or my Pascal/Wirth early origins, but I personally haven’t ever needed to use it. Glad to hear it’s found some uses outside of the US military.
- deleted 5y ago[deleted]
- Unklejoe 5y agoYep. Another thing to watch out for is using bitfields to access device registers. One time, I was working with an older embedded PPC architecture on a driver to talk to an FPGA that was attached to a 32 bit local bus. The problem is that it didn’t support unaligned accesses. The lower two address bits simply weren’t hooked up, so any access to byte addresses that weren’t a multiple of 4 would just behave as if you masked the two lower address bits off. There was a structure that used bitfields to access bits in a register on that FPGA. It worked fine til I updated GCC, then it stopped working. It turns out that the newer version of GCC would do a single byte unaligned read if you were accessing, say, bits 8-15 in a 32 bit bitfield, whereas the older GCC would read the full 32 bit word and shift/mask as needed. It turns out you can force the older behavior with -fstrict-volatile-bitfields. Took a minute to figure out, but I learned my lesson. That said, I don’t think you should really be doing IO that way anyway. I typically use IO accessor methods, sometimes with raw addresses, sometimes with struct overlays and taking the address of the member.