Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why class size increases when int64_t changes to int32_t

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?

like image 754
xinaiz Avatar asked Jul 12 '16 17:07

xinaiz


3 Answers

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.

like image 75
NathanOliver Avatar answered Oct 19 '22 17:10

NathanOliver


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.

like image 30
supercat Avatar answered Oct 19 '22 17:10

supercat


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.

like image 6
Justin Time - Reinstate Monica Avatar answered Oct 19 '22 15:10

Justin Time - Reinstate Monica