当int64_t更改为int32_t时,为什么类大小会增加?

28

在我的第一个例子中,我使用了两个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上都会发生。但为什么?


11
大小增加,而不是对齐。在第一种情况下,第一个和第二个变量都属于同一个int64_t类型。在第二种情况下,它们显然不能这样做。 - Marc Glisse
lorond,没有编译标志,我也在MSVC 2015上进行了测试,它也打印出8和16。 - xinaiz
1
@MarcGlisse这只有在你知道标准禁止将位字段嵌入到不匹配类型的未使用字节中时才是显而易见的,正如supercat的答案所述。由于没有技术原因(据我所知)可以解释这种禁止,因此对于这一点的“显而易见”并不清楚(事实上,在阅读supercat的答案之前我也不知道这个限制)。 - Kyle Strand
1
@KyleStrand 看起来我说的不仅“不明显”,而且实际上是错误的,因为令人惊讶的是,许多 ABIs 确实会压缩并在第二种情况下给出大小 8。我在这里学到了一些东西。如果你将其分成 20+20+24,事情会变得更有趣,在 linux-x86_64 上,所有重要的事情都在于中间字段使用的类型是 32 位(大小为 12)还是 64 位(大小为 8)。 - Marc Glisse
1
@underscore_s 抱歉,我是说冒号。我是C++的新手,只是对这个从未被使用过的东西感到好奇。冷静点。 - user6245072
显示剩余7条评论
4个回答

37

在您的第一个示例中

int64_t first : 40;
int64_t second : 24;

第一和第二个示例都使用单个64位整数的64位。这导致类的大小为单个64位整数。在第二个示例中,您有

int64_t first : 40;
int32_t second : 24;

这是将两个单独的位字段存储在两个不同的内存块中。你使用了64位整数的40位,然后再使用了另一个32位整数的24位。这意味着你至少需要12个字节(此示例使用8位字节)。最有可能的是你看到的额外4个字节是为了将类对齐到64位边界而进行的填充。

正如其他答案和评论所指出的,这是实现定义行为,你在不同的实现中可能会看到不同的结果。


2
据我所知,这里唯一相关的事实是supercat提到的C标准要求:“位域必须存储在指定类型或其有符号/无符号等价物的对象内。”你的回答似乎暗示int32_t受到比int64_t更严格的对齐约束,这是没有意义的。 - Kyle Strand
1
@KyleStrand,我并不是在暗示那个。我的意思是编译器添加了另外4个字节到结构体中,以使得结构体大小可以被8整除。这样当存储在数组中时,第一个int64_t就可以跨越两个不同的8字节块。 - NathanOliver
4
我说过了为什么。编译器将其视为单独的位域,因此不会将它们组合在一起。另外,就supercat的引用而言,那是关于C语言的,而这个问题标记为C++语言。 C++并没有继承整个C标准。我无法在C++标准中找到与C语言相同的要求。 - NathanOliver
2
你说第二个例子是“两个不同内存块中存储的两个独立字段”,其中你可能指的是“独立块”是指包含对象(原始类型)是独立的,而在原始版本中只有一个单一的int64_t原始类型表示一个内存“块”。(这很模糊,因为人们可能认为原始版本的位域本身分别为40位和24位的“独立块”。)但你没有说明为什么位域不能在一个单一的内存“块”中表示。这是supercat解释的关键点。 - Kyle Strand
3
C++可能已经继承了从C语言中可能存在的要求,但我今天早上不想去查找标准文件--不过在ABI文档中明确规定并且至少在C++标准中隐含着一个要求,即对于POD结构体(本例就是)必须按照与C等效的方式进行布局。如果不这样做,C和C++之间的函数调用互操作性将会是不可能的(通常情况下)。 - zwol
显示剩余4条评论

15

C标准对于位域(bitfields)的规则并不足以向程序员提供有用的关于内存布局方面的信息,但它却限制了实现在其他情况下可能会很有用的自由。

特别是,位域需要被存储在被指定类型的对象之内,或者是其带符号/无符号等效物。在第一个例子中,第一个位域必须存储在int64_t或uint64_t对象中,第二个位域同理,但它们可以放在同一个对象中。在第二个例子中,第一个位域必须存储在int64_t或uint64_t中,第二个位域必须存储在int32_t或uint32_t中。即使在结构体末尾添加其他位域,uint64_t仍将拥有24个"搁浅"的位;uint32_t有8个当前未使用的位,但如果添加了比8位小的另一个int32_t或uint32_t位域,则这些位将可用于该位域。

我认为标准在编译器自由和程序员有用信息/控制方面做出了最糟糕的平衡,但事实就是如此。个人认为,如果首选语法允许程序员根据普通对象的布局(例如,位域“foo”应存储在“foo_bar”字段的第4位(值为16)开始的3位中),精确地指定其布局,则位域将更加有用,但我知道没有计划在标准中定义这样的东西。


1
"位域必须存储在指定类型或其带符号/无符号的等效对象中。引用标准来支持这一点是有必要的。" - CodesInChaos
6
C11:“实现可以分配足够大的可寻址存储单元来容纳位域。如果有足够的空间,紧随结构体中另一个位域后面的位域将被打包到同一存储单元的相邻位中。如果剩余空间不足,未能容纳的位域是放在下一个存储单元内还是与相邻存储单元重叠是由实现定义的。” 我没有找到任何关于必须在声明类型的对象内存储的要求。 - Marc Glisse
1
在之前的标准中,Supercat,可能有这样的限制吗? - Kyle Strand
1
@KyleStrand:我在那里找不到它,也不记得我在哪里读到了位域的行为描述;我曾经认为我在标准或引用标准的某些东西中读到过它,但我认为它似乎是不合逻辑的。也许我在K&R1或K&R2中读到了它,但我观察到了许多编译器中的这种行为,因此无论编译器是否需要以这种方式工作,这是许多编译器实际上所做的。 - supercat
1
@KyleStrand:我认为这种行为在K&R1中已经描述过了,而且由于它在C89下是允许的,所以许多编译器保留了这种行为。在需要32位类型的32位对齐的机器上,使用32位基础类型存储六个6位字段将占用八个字节,而使用16位基础类型存储这些字段仅需要六个字节。位域的最佳布局通常取决于其后面的内容,但由于“公共初始序列”规则有效地禁止编译器在布局结构时考虑到这一点,因此旧的行为可以... - supercat
1
这在那些无法有效处理跨越对齐边界的位域的平台上非常有用(在能够有效处理这种位域的平台上,最好将它们尽可能地紧密打包)。 - supercat

6

补充其他人已经说过的:

如果你想检查它,你可以使用编译器选项或外部程序输出结构体布局。

考虑这个文件:

// 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前端命令),我们可以将Test_1Test_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版本(由Rextester使用的版本)似乎默认将两个位域优化为单个变量,我不确定如何禁用此行为。


5

标准规定:

§ 9.6 位域

类对象内的位域分配是由具体实现定义的。位域对齐也是由具体实现定义的。[注意: 在一些机器上,位域跨越分配单元,而在另一些机器上则不会。在一些机器上,位域从右至左分配,在另一些机器上则从左至右。 —结束注释]

C++11论文

因此布局取决于编译器实现、编译选项、目标架构等等。只检查了几个编译器,大多数输出都是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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接