28

In my first example I have two bitfields using int64_t. When I compile and get the size of the class I get 8.

class Test
{
    int64_t first : 40;
    int64_t second : 24;
};

int main()
{
    std::cout << sizeof(Test); // 8
}

But when I change the second bitfeild to be a int32_t the size of the class doubles to 16:

class Test
{
    int64_t first : 40;
    int32_t second : 24;
};

int main()
{
    std::cout << sizeof(Test); // 16
}

This happens on both GCC 5.3.0 and MSVC 2015. But why?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
xinaiz
  • 7,744
  • 6
  • 34
  • 78
  • 11
    **Size** increases, not alignment. In the first case, first and second are part of the same int64_t. In the second case, they obviously cannot. – Marc Glisse Jul 12 '16 at 17:56
  • Try to get addresses of fields, or even better - post generated assembly of code accessing both fields. Or at least - what compiller do you use? – lorond Jul 12 '16 at 18:04
  • @BlackMoses https://godbolt.org/g/PUL0qB it generates 8 for both cases. Any compilations flags? – lorond Jul 12 '16 at 18:14
  • @BlackMoses actually this alligment is entirely up to compiller implementation, it's setting and so on. – lorond Jul 12 '16 at 18:17
  • lorond, no compilation flags, I tested it on MSVC 2015 too and it also prints 8 and 16 – xinaiz Jul 12 '16 at 18:23
  • 1
    @MarcGlisse This is only obvious if you know that the standard forbids embedding bitfields in unused bytes of non-matching types, as per supercat's answer. Since there's no technical reason (AFAIK) for this prohibition, it's unclear how this is "obvious" (and indeed I didn't know about this restriction until reading supercat's answer). – Kyle Strand Jul 13 '16 at 00:02
  • What's the meaning of the semicolon in `int64_t first : 40;`? – user6245072 Jul 13 '16 at 08:50
  • @user6245072 Are you familiar with C++ at all? The semicolon `;` ends a statement. The _colon_ `:` indicates a bitfield declaration. This isn't really the place to ask such basic questions. – underscore_d Jul 13 '16 at 10:37
  • 1
    @KyleStrand it seems that what I said was not only "not obvious", it was actually wrong, since surprisingly many ABIs do seem to compress and give size 8 in the second case. I learned something here. Things become even more fun if you split into 20+20+24, where on linux-x86_64 all that matters is whether the type used for the middle field is 32 bits (size 12) or 64 bits (size 8). – Marc Glisse Jul 13 '16 at 11:06
  • 1
    @underscore_s sorry, I meant colon. I am a newbie to C++ and was just curios about that, which I never saw used like this. Chill down. – user6245072 Jul 13 '16 at 11:31
  • You can use `alignas` to force the size. –  Jul 13 '16 at 13:15
  • @user6245072 That's understandable, I think; I had forgotten the details of how bitfield declarations work myself, and googling "c++ member declaration colon" left a lot to be desired. – Kyle Strand Jul 13 '16 at 14:25

4 Answers4

37

In your first example

int64_t first : 40;
int64_t second : 24;

Both first and second use the 64 bits of a single 64 bit integer. This causes the size of the class to be a single 64 bit integer. In the second example you have

int64_t first : 40;
int32_t second : 24;

Which is two separate bit fields being stored in two different chunks of memory. You use 40 bits of the 64 bit integer and then you use 24 bit of another 32 bit integer. This means you need at least 12 bytes(this example is using 8 bit bytes). Most likely the extra 4 bytes you see is padding to align the class on 64 bit boundaries.

As other answers and comments have pointed out this is implementation defined behavior and you can/will see different results on different implementations.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 2
    As far as I can tell, the *only* relevant fact here is the C Standard's requirement mentioned by supercat: "bitfields are required to be stored *within* objects that are of the indicated types, or the signed/unsigned equivalent thereof." Your answer seems to imply that `int32_t` is somehow subject to an *alignment* constraint that is *more* strict than those imposed on `int64_t`, which makes no sense. – Kyle Strand Jul 13 '16 at 00:01
  • 1
    @KyleStrand That is not what I am implying. I am implying that the compiler added another 4 bytes to the struct to make the struct size divisible by 8. This way when stored in an array the first `int64_t` does span two different 8 byte blocks. – NathanOliver Jul 13 '16 at 00:04
  • That would make sense as an answer if the original question were "why is the size 16 instead of 12, i.e., the total size of `int32_t` plus `int64_t`." But in fact OP was asking why the size increases *at all*, i.e., why the `int32_t` bitfield cannot be contained *inside of * the `int64_t` type, since there's sufficient room to store both bitfields within 8 bits (since the bitfield sizes themselves didn't change). – Kyle Strand Jul 13 '16 at 00:08
  • 4
    @KyleStrand And I said why. The compiler is treating it as a separate bit field thus it does not combine them together. Also as far as supercat's quote goes that is for C and this question is tagged as C++. C++ does not inherit the whole C standard. I cannot find the same requirement C has in the C++ standard. – NathanOliver Jul 13 '16 at 00:34
  • 2
    You stated that the second example is "two separate fields being stored in two different chunks of memory," where presumably by "separate chunks" you meant that the *containing objects* (the primitive types) are separate, whereas in the original there is only one single `int64_t` primitive representing a "chunk" of memory. (This is vague because one might consider the original's bitfields themselves of 40 and 24 bits each to be "separate chunks.") But you don't state *why* the bitfields can't be represented in one single "chunk" of memory. This is the crucial bit that supercat explains. – Kyle Strand Jul 13 '16 at 00:49
  • 1
    If that requirement is not in the C++ standard, then presumably *there is no reason* why the second example must have a larger total size. – Kyle Strand Jul 13 '16 at 00:50
  • @Kylestrand. Exactly. There is no reason. All I did was detail what happened. As other answers have shown some implementations give 8 for both examples. Bit field are largely implementation defined behavior. I believe the standard section only has 4 paragraphs. All I tried to do here is explain to the OP why you could have 16 bytes. – NathanOliver Jul 13 '16 at 01:09
  • 3
    C++ may or may not have carried over a requirement from C that may or may not itself exist -- I'm not in the mood to go digging through standardese this morning -- but there *is* a requirement, spelled out in the ABI document and at least implicit in the C++ standard, for POD structures (which this is) to be laid out exactly as the C equivalent would have been. If this were not the case, function-call interoperability between C and C++ would be impossible (in general). – zwol Jul 13 '16 at 12:59
  • I hadn't noticed the answers showing implementations that do use 8 bytes; thanks for pointing that out. It still seems to me that the key part of the question is why the two bitfields are stored in separate 8-byte segments in the second example, and you *have not* explained this; you've merely stated it (in your "chunks" sentence). Given that, as we've both stated, there's no reason not to store both in a single "chunk" of 8 bytes, most of your answer just doesn't seem helpful; only your last paragraph is actually important. – Kyle Strand Jul 13 '16 at 14:34
15

The C Standard's rules for bitfields aren't precise enough to tell programmers anything useful about the layout, but nonetheless deny implementations what might otherwise be useful freedoms.

In particular, bitfields are required to be stored within objects that are of the indicated types, or the signed/unsigned equivalent thereof. In your first example, the first bitfield must be stored in an int64_t or uint64_t object, and the second one likewise, but there's enough room for them to fit into the same object. In the second example, the first bitfield must be stored in an int64_t or uint64_t, and the second one in an int32_t or uint32_t. The uint64_t will have 24 bits that would be "stranded" even if additional bit fields were added to the end of the struct; the uint32_t has 8 bits which aren't presently used, but would be available for use of another int32_t or uint32_t bitfield whose width was less than 8 were added to the type.

IMHO, the Standard strikes just about the worst possible balance here between giving compilers freedom vs giving programmers useful information/control, but it is what it is. Personally I think bitfields would be much more useful if the preferred syntax let programmers specify their layout precisely in terms of ordinary objects (e.g. bitfield "foo" should be stored in 3 bits, starting at bit 4 (value of 16), of field "foo_bar") but I know of no plans to define such a thing in the Standard.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • 1
    "bitfields are required to be stored within objects that are of the indicated types, or the signed/unsigned equivalent thereof. " quoting the standard to back up this claim wouldn't hurt. – CodesInChaos Jul 13 '16 at 10:58
  • 6
    C11: "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." I didn't find anything about being forced to store within object of the declared type. – Marc Glisse Jul 13 '16 at 11:14
  • 1
    Was there perhaps such a restriction in a previous standard, supercat? – Kyle Strand Jul 13 '16 at 14:36
  • 1
    @KyleStrand: I can't find it in there, and don't remember where I read the behavior of bitfields described; I'd thought I'd read it in either the Standard or something that was quoting the Standard, but I thought it seemed illogical. Perhaps I read it in K&R1 or K&R2, but I've observed the behavior in a number of compilers, so regardless of whether compilers are *required* to work that way, it's what a lot of them *do*. – supercat Jul 13 '16 at 19:24
  • 1
    @KyleStrand: I think the behavior was described in K&R1 and, since it's permissible under C89, many compilers have retained that behavior. On a machine requiring 32-bit alignment for 32-bit types, storing six 6-bit fields using a 32-bit underlying type would take eight bytes, while storing those fields using a 16-bit underlying type would only take six. The optimal layout of bit fields will often depend upon what follows them, but since the Common Initial Sequence rule effectively forbids compilers from taking that into account when laying out structures, the old behavior can be... – supercat Jul 14 '16 at 19:00
  • 1
    ...useful on platforms which cannot efficiently handle bitfields that straddle alignment boundaries (on platforms that can efficiently handle such bitfields, it would often be better to simply pack them as tightly as possible). – supercat Jul 14 '16 at 19:01
6

To add to what others have already said:

If you want to examine it, you can use a compiler option or external program to output the struct layout.

Consider this file:

// test.cpp
#include <cstdint>

class Test_1 {
    int64_t first  : 40;
    int64_t second : 24;
};

class Test_2 {
    int64_t first  : 40;
    int32_t second : 24;
};

// Dummy instances to force Clang to output layout.
Test_1 t1;
Test_2 t2;

If we use a layout output flag, such as Visual Studio's /d1reportSingleClassLayoutX (where X is all or part of the class or struct name) or Clang++'s -Xclang -fdump-record-layouts (where -Xclang tells the compiler to interpret -fdump-record-layouts as a Clang frontend command instead of a GCC frontend command), we can dump the memory layouts of Test_1 and Test_2 to standard output. [Unfortunately, I'm not sure how to do this directly with GCC.]

If we do so, the compiler will output the following layouts:

  • Visual Studio:
cl /c /d1reportSingleClassLayoutTest test.cpp

// Output:
tst.cpp
class Test_1    size(8):
    +---
 0. | first (bitstart=0,nbits=40)
 0. | second (bitstart=40,nbits=24)
    +---



class Test_2    size(16):
    +---
 0. | first (bitstart=0,nbits=40)
 8. | second (bitstart=0,nbits=24)
    | <alignment member> (size=4)
    +---
  • Clang:
clang++ -c -std=c++11 -Xclang -fdump-record-layouts test.cpp

// Output:
*** Dumping AST Record Layout
   0 | class Test_1
   0 |   int64_t first
   5 |   int64_t second
     | [sizeof=8, dsize=8, align=8
     |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout
Record: CXXRecordDecl 0x344dfa8 <source_file.cpp:3:1, line:6:1> line:3:7 referenced class Test_1 definition
|-CXXRecordDecl 0x344e0c0 <col:1, col:7> col:7 implicit class Test_1
|-FieldDecl 0x344e1a0 <line:4:2, col:19> col:10 first 'int64_t':'long'
| `-IntegerLiteral 0x344e170 <col:19> 'int' 40
|-FieldDecl 0x344e218 <line:5:2, col:19> col:10 second 'int64_t':'long'
| `-IntegerLiteral 0x344e1e8 <col:19> 'int' 24
|-CXXConstructorDecl 0x3490d88 <line:3:7> col:7 implicit used Test_1 'void (void) noexcept' inline
| `-CompoundStmt 0x34912b0 <col:7>
|-CXXConstructorDecl 0x3490ee8 <col:7> col:7 implicit constexpr Test_1 'void (const class Test_1 &)' inline noexcept-unevaluated 0x3490ee8
| `-ParmVarDecl 0x3491030 <col:7> col:7 'const class Test_1 &'
`-CXXConstructorDecl 0x34910c8 <col:7> col:7 implicit constexpr Test_1 'void (class Test_1 &&)' inline noexcept-unevaluated 0x34910c8
  `-ParmVarDecl 0x3491210 <col:7> col:7 'class Test_1 &&'

Layout: <CGRecordLayout
  LLVMType:%class.Test_1 = type { i64 }
  NonVirtualBaseLLVMType:%class.Test_1 = type { i64 }
  IsZeroInitializable:1
  BitFields:[
    <CGBitFieldInfo Offset:0 Size:40 IsSigned:1 StorageSize:64 StorageOffset:0>
    <CGBitFieldInfo Offset:40 Size:24 IsSigned:1 StorageSize:64 StorageOffset:0>
]>

*** Dumping AST Record Layout
   0 | class Test_2
   0 |   int64_t first
   5 |   int32_t second
     | [sizeof=8, dsize=8, align=8
     |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout
Record: CXXRecordDecl 0x344e260 <source_file.cpp:8:1, line:11:1> line:8:7 referenced class Test_2 definition
|-CXXRecordDecl 0x344e370 <col:1, col:7> col:7 implicit class Test_2
|-FieldDecl 0x3490bd0 <line:9:2, col:19> col:10 first 'int64_t':'long'
| `-IntegerLiteral 0x344e400 <col:19> 'int' 40
|-FieldDecl 0x3490c70 <line:10:2, col:19> col:10 second 'int32_t':'int'
| `-IntegerLiteral 0x3490c40 <col:19> 'int' 24
|-CXXConstructorDecl 0x3491438 <line:8:7> col:7 implicit used Test_2 'void (void) noexcept' inline
| `-CompoundStmt 0x34918f8 <col:7>
|-CXXConstructorDecl 0x3491568 <col:7> col:7 implicit constexpr Test_2 'void (const class Test_2 &)' inline noexcept-unevaluated 0x3491568
| `-ParmVarDecl 0x34916b0 <col:7> col:7 'const class Test_2 &'
`-CXXConstructorDecl 0x3491748 <col:7> col:7 implicit constexpr Test_2 'void (class Test_2 &&)' inline noexcept-unevaluated 0x3491748
  `-ParmVarDecl 0x3491890 <col:7> col:7 'class Test_2 &&'

Layout: <CGRecordLayout
  LLVMType:%class.Test_2 = type { i64 }
  NonVirtualBaseLLVMType:%class.Test_2 = type { i64 }
  IsZeroInitializable:1
  BitFields:[
    <CGBitFieldInfo Offset:0 Size:40 IsSigned:1 StorageSize:64 StorageOffset:0>
    <CGBitFieldInfo Offset:40 Size:24 IsSigned:1 StorageSize:64 StorageOffset:0>
]>

Note that the version of Clang I used to generate this output (the one used by Rextester) appears to default to optimising both bitfields into a single variable, and I'm unsure how to disable this behaviour.

5

Standard says:

§ 9.6 bit-fields

Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. [Note: Bit-fields straddle allocation units on some machines and not on others. Bit-fields are assigned right-to-left on some machines, left-to-right on others. — end note]

c++11 paper

So layout depends on compiler implementation, compilation flags, target arch and so on. Just checked several compilers and output mostly is 8 8:

#include <stdint.h>
#include <iostream>

class Test32
{
    int64_t first : 40;
    int32_t second : 24;
};

class Test64
{
    int64_t first : 40;
    int64_t second : 24;
};

int main()
{
    std::cout << sizeof(Test32) << " " << sizeof(Test64);
}
Community
  • 1
  • 1
lorond
  • 3,856
  • 2
  • 37
  • 52