Why class size increases when int64_t changes to int32_t
Asked Answered
T

4

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?

Trainband answered 12/7, 2016 at 17:54 Comment(12)
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.Slink
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?Damarisdamarra
@BlackMoses godbolt.org/g/PUL0qB it generates 8 for both cases. Any compilations flags?Damarisdamarra
@BlackMoses actually this alligment is entirely up to compiller implementation, it's setting and so on.Damarisdamarra
lorond, no compilation flags, I tested it on MSVC 2015 too and it also prints 8 and 16Trainband
@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).Lyophilize
What's the meaning of the semicolon in int64_t first : 40;?Coggins
@Coggins 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.Kalgoorlie
@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).Slink
@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.Coggins
You can use alignas to force the size.Solicit
@Coggins 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.Lyophilize
C
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.

Connecticut answered 12/7, 2016 at 17:58 Comment(9)
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.Lyophilize
@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.Connecticut
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).Lyophilize
@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.Connecticut
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.Lyophilize
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.Lyophilize
@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.Connecticut
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).Billfold
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.Lyophilize
D
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.

Deprivation answered 12/7, 2016 at 18:12 Comment(6)
"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.Romanticist
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.Slink
Was there perhaps such a restriction in a previous standard, supercat?Lyophilize
@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.Deprivation
@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...Deprivation
...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).Deprivation
E
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.

Expiratory answered 12/7, 2016 at 21:26 Comment(0)
D
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);
}
Damarisdamarra answered 12/7, 2016 at 18:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.