Подтвердить что ты не робот

Почему размер класса увеличивается, когда int64_t изменяется на int32_t

В моем первом примере у меня есть два битовых поля, используя int64_t. Когда я компилирую и получаю размер класса, я получаю 8.

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

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

Но когда я изменяю второй битфейд как int32_t, размер класса удваивается до 16:

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

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

Это происходит как на GCC 5.3.0, так и на MSVC 2015. Но почему?

4b9b3361

Ответ 1

В первом примере

int64_t first : 40;
int64_t second : 24;

Оба first и second используют 64 бита одного 64-битного целого числа. Это приводит к тому, что размер класса должен быть единственным 64-битным целым числом. Во втором примере вы

int64_t first : 40;
int32_t second : 24;

Какое два отдельных битовых поля сохраняются в двух разных кусках памяти. Вы используете 40 бит из 64-битного целого числа, а затем используете 24 бита из другого 32-битного целого числа. Это означает, что вам нужно не менее 12 байтов (в этом примере используются 8-битные байты). Скорее всего, дополнительные 4 байта, которые вы видите, дополняют, чтобы выровнять класс на 64-битных границах.

Как указывалось в других ответах и ​​комментариях, это поведение, определяемое реализацией, и вы можете/увидеть различные результаты в разных реализациях.

Ответ 2

Правила C Standard для битовых полей недостаточно точны, чтобы сообщить программистам что-нибудь полезное в макете, но тем не менее отрицают реализации, которые могли бы быть полезными свободами.

В частности, битовые поля должны храниться в объектах с указанными типами или в эквиваленте, подписанном/без знака. В первом примере первое битовое поле должно быть сохранено в объекте int64_t или uint64_t, а второй - аналогичным образом, но для них достаточно места для тот же объект. Во втором примере первое поле бит должно быть сохранено в int64_t или uint64_t, а второй - в int32_t или uint32_t. Uint64_t будет иметь 24 бита, которые будут "застряли", даже если в конец структуры добавлены дополнительные битовые поля; uint32_t имеет 8 бит, которые в настоящее время не используются, но будут доступны для использования другого битового поля int32_t или uint32_t, ширина которого была меньше 8.

IMHO, Стандарт поражает только худшее возможное равновесие между предоставлением компиляторам свободы, давая программистам полезную информацию/контроль, но это то, что есть. Лично я думаю, что битполы были бы намного более полезными, если бы предпочтительный синтаксис позволял программистам задавать свой макет точно в терминах обычных объектов (например, битполе "foo" должно храниться в 3 битах, начиная с бит 4 (значение 16), поля "foo_bar" ), но я не знаю, как это определить в стандарте.

Ответ 3

Чтобы добавить к тому, что уже сказали другие:

Если вы хотите его изучить, вы можете использовать параметр компилятора или внешнюю программу для вывода компоновки структуры.

Рассмотрим этот файл:

// 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;

Если мы используем флаг вывода макета, такой как Visual Studio /d1reportSingleClassLayoutX (где X - это все или часть имени класса или структуры) или Clang++ -Xclang -fdump-record-layouts (где -Xclang говорит компилятору интерпретировать -fdump-record-layouts как команда фронтмена Clang вместо команды GCC frontend), мы можем сбросить макеты памяти Test_1 и Test_2 до стандартного вывода. [К сожалению, я не уверен, как это сделать непосредственно с GCC.]

Если мы это сделаем, компилятор выведет следующие макеты:

  • 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>
]>

Обратите внимание, что версия Clang I, используемая для генерации этого вывода (тот, который используется Rextester) по умолчанию, оптимизирует оба битовых поля в одну переменную, и я не уверен, как отключить это поведение.

Ответ 4

Стандарт говорит:

§ 9.6 бит-поля

Распределение бит-полей внутри класса объект определяется реализацией. Выравнивание битовых полей определяется реализацией. [Примечание: Бит-поля расположены на некоторых машинах, а не на других. Бит-поля назначаются справа налево на некоторых машинах, слева направо на других. - конечная нота]

С++ 11 paper

Таким образом, макет зависит от реализации компилятора, флагов компиляции, целевой арки и т.д. Просто проверил несколько компиляторов, и в основном вывод 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);
}