问个关于内存对齐的问题

2021-10-23 01:44:49 +08:00
 mingl0280

为啥

struct FixedLengthHeader {
        uint32_t HeaderSize = 0;
        uint64_t CryptogramSize = 0;
        uint64_t ReservedField = 0;
}FixedPackageHeaders;

占用 20 字节( 4+8+8 ),但是如果用下面这个写法,

    struct FixedLengthHeader {
        uint32_t HeaderSize = 0;
        uint64_t CryptogramSize = 0;
        uint8_t DevFlag = 0;
        uint8_t HeaderVer = 0;
        uint32_t PackagerVer = 0;
        uint16_t Reserved = 0
    }FixedPackageHeaders;

会因为内存对齐占用 24 字节(4+8+2+2+4+4)的内存呢……

3119 次点击
所在节点    C++
23 条回复
secondwtq
2021-10-23 01:50:01 +08:00
你这个 FixedLengthHeader 在我这是 24 啊 ...

http://www.catb.org/esr/structure-packing/
mingl0280
2021-10-23 01:56:45 +08:00
@secondwtq 啊抱歉,应该说是 MSVC/g++,64 位。
我也不知道为啥第一个会只用了 20 字节,第二个用了 24,之前搞得我很蛋疼(我以为第一个也是 24bytes )
secondwtq
2021-10-23 02:11:28 +08:00
你是不是#pragma pack(4)了
mingl0280
2021-10-23 02:56:26 +08:00
@secondwtq 没有,两个写法都没有。
就是因为没加 Pack 才会觉得奇怪的。
dangyuluo
2021-10-23 03:41:32 +08:00
你可以试着看看每个成员的 offset 。不过我测试 MSVC, Gcc, Clang 下的 sizeof 都是 24

https://godbolt.org/z/c1TM4778j
secondwtq
2021-10-23 03:50:11 +08:00
我猜可能哪个 header 里面 #pragma pack push 了忘 pop 了 ... 你可以 -E 一下看看。实在不行一点点 reduce,能 reduce 到放在 godbolt 上面的程度更好

你这个大小是怎么观察到的?是直接 sizeof 还是?

另外 Clang 有试过么? Clang 有个好处是想要折腾比较方便,比如我加个 #pragma pack 就可以在 AST dump 里看到: https://gist.github.com/secondwtq/e5ce6e72fe80900d54fa0eb44d4487d4
LifStge
2021-10-23 06:59:14 +08:00
测试代码 用最干净的测试 或者自己有对齐要求的 就主动 push 设置 然后在 pop 你无法保证加进来的其他代码是否改变了默认值
不要因为未知的或可能的设置 来得出自认为有问题的结论
smdbh
2021-10-23 07:09:16 +08:00
一般来说,例如 uint32_t 需要在整除 4 的地址上,16 就是整除 2 的,HeaderVer 后面有 2 个字节的空隙,PackagerVer 才对齐 。
Ediacaran
2021-10-23 08:36:30 +08:00
低于 32 位的类型后面会空出位置,你可以用调试器或者用 offsetof 宏看一下每个成员的偏移
非对其访问需要额外的指令操作,所以除非声明了 pack,否则编译器会默认对齐
elfive
2021-10-23 08:50:23 +08:00
这种对对齐字节有要求的场景请务必手动指定对齐字节位……
因为在编译时,这段代码很有可能会受到
1. 编译器默认对齐字节数
2. 代码中其他地方定义的对齐数
的影响而产生不可预计的影响。

字节对齐的语句也要成对出现,避免对其他地方的影响:
#pragma pack(n) // 设置对齐字节数
#pragma pack() // 取消设置,恢复默对齐字节数

或者:
#pragma pack(push)
#pragma pack(n)
#pragma pop()
elfive
2021-10-23 08:51:34 +08:00
@elfive #10 最后那个#pragma pop 写错了,应该是#pragma pack(pop)
mingl0280
2021-10-23 09:23:17 +08:00
@secondwtq 生成出来的二进制文件我按着字节看的,第一个 int 是 0x14,真就是 20 字节。看到的时候人都给我搞懵逼了……
mingl0280
2021-10-23 09:25:06 +08:00
@secondwtq 该文件引用的头文件中(只有一个十来行的自定义头文件)并没有其他的 pack 指令(其它头文件都是标准库),我挨个核对过
@elfive
NoAnyLove
2021-10-23 09:51:16 +08:00
没有用紧凑声明的话就会默认 32 位对齐,出于性能考虑,至少 32 位下是这么的。话说在 64 位中也是 32 位对齐吗?

uint32_t HeaderSize: 4
uint64_t CryptogramSize: 8
uint8_t DevFlag: 1
uint8_t HeaderVer: 1
uint8_t __padding: 1
uint8_t __padding: 1
uint32_t PackagerVer: 4
uint16_t Reserved: 2
uint8_t __padding: 1
uint8_t __padding: 1

4+8+1+1+1+1+4+2+1+1 = 24,好像没啥问题
jones2000
2021-10-23 10:24:15 +08:00
强制 1 字节对齐
westtide
2021-10-23 11:07:26 +08:00
ABI 规范只定义了变址的 对齐方式,并没有定义变蜇的分配顺序 编译器可以自由决定使用何种顺序来分配变量 。
对于由基本数据类型构造而成的 struct 结构体数据,为了保证其中每个成员都满足对齐要 求,i386 System V ABI 对 strucl 结构体数据的对齐方式有如下几条规则:
整个结构体变显的 对齐方式与其中对齐方式最严格的成员相同;
每个成员在满足其对齐方式的前提下,取最小的可用位置作为成员在结构体中的偏移址,这可能导致内部插空;
结构体大小应为对齐 边界长度的整数倍,这可能导致尾部插空 。

《计算机系统基础 第 2 版 袁春风余子濠 © 编著》
liuxu
2021-10-23 11:17:31 +08:00
你下面的不是
24 字节(4+8+2+2+4+4 )
而是
24 字节(4+8+4+4+4 )

你试试
struct FixedLengthHeader {
uint32_t HeaderSize = 0;
uint64_t CryptogramSize = 0;
uint8_t DevFlag = 0;
uint8_t HeaderVer = 0;
uint16_t Reserved = 0;
uint32_t PackagerVer = 0;
}FixedPackageHeaders;

应该会变成 4+8+4+4=20

应该是编译器优化结果

8|8|32 的位地址占用情况应该是
1111 1111 1111 1111 xxxx xxxx xxxx xxxx
1111 1111 1111 1111 1111 1111 1111 1111
也就是 2 个 8 后面空着不要了

但是如果 8|8|8/16|32,不会再多分配内存,继续复用没有用的 16 位空间
bigHentai
2021-10-23 16:35:30 +08:00
我记得默认是按结构体中最大那个变量的字节数对齐,所以是 uint64_t,也就是 8 字节对齐,上面那个默认应该是 24 ,下面的应该是 32 ,你应该是开了强制 4 字节对齐之类的?
liuxu
2021-10-24 14:01:37 +08:00
@liuxu #17 修正一下,我之前是 32 位系统 debug 的,现在都是 64 位系统,情况有变化。

首先说结论:
1. 和编译的目标程序的位数有关,32 位程序最高是 4 字节(32 位)对齐,64 位程序最高是(8 字节)64 位对齐。
2. 在 1 的要求下,struct 中按最宽位数的变量对齐。结合 1 中的“最高”的意思是:
64 位程序:uint32_t + uint64_t + uint64_t 分配 24 字节(8+8+8)。
32 位程序:uint32_t + uint64_t + uint64_t 分配 20 字节(4+(4+4)+(4+4))。
32/64 位程序:uint8_t +uint16_t +uint8_t 分配 8 字节(2+2+2)。
3. 添加#pragma pack(4)后,32 位和 64 位程序都按 4 字节(32 位)对齐,也就是 uint32_t + uint64_t + uint64_t 都是 20 字节(4+(4+4)+(4+4))。但是如果 struct 最大为 uint16_t ,则依然按 2 字节对齐。(也就是说 pack 无法影响 struct 最大位宽限制)
4. 添加了#pragma pack(16)后,64 位依然按 8 字节(64 位)对齐。也就是说 pack 中的数字只能是不大于(系统位数 /8)的 2 的次方的数字。


所以楼主的情况, 如果代码中真的没有#pragma pack(),但是出现了帖子中的现象,只有可能是以下 3 中情况:
1. 楼主的系统是 32 位的。( 32 位系统只会运行 32 位编译器编译出 32 位程序)
2. 楼主的编译器是 32 位的。( 32 位编译器只会编译出 32 位程序)
3. 楼主 64 位 msvc 编译目标选择的是 32 位程序。(据我所知 vs 默认的 debuging 版本编译的是 32 位程序,至少我几年前 debug 的时候是的,也有可能是我创建项目的时候设置成了 32 位)。

即楼主 2 次编译的字节对齐方式为:
struct FixedLengthHeader {
uint32_t HeaderSize = 0;
uint64_t CryptogramSize = 0;
uint64_t ReservedField = 0;
}FixedPackageHeaders;
20 字节(4+(4+4)+(4+4))。

struct FixedLengthHeader {
uint32_t HeaderSize = 0;
uint64_t CryptogramSize = 0;
uint8_t DevFlag = 0;
uint8_t HeaderVer = 0;
uint32_t PackagerVer = 0;
uint16_t Reserved = 0
}FixedPackageHeaders;
24 字节(4+(4+4)+4+4+4)。

以上是 64 位 linux gcc 下的结果推测的 msvc 的结果,windows 下的实际结果还是得大佬们自己调试。



以下是我 GDB 打印情况:
系统:Ubuntu 20.04.3 LTS x86_64
gcc: gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0


没有添加#progma pack(4),编译指令:gcc -g test.c

/* offset | size */ type = struct FixedLengthHeader {
/* 0 | 4 */ uint32_t HeaderSize;
/* XXX 4-byte hole */
/* 8 | 8 */ uint64_t CryptogramSize;
/* 16 | 8 */ uint64_t ReservedField;

/* total size (bytes): 24 */
}
(8+8+8)

/* offset | size */ type = struct FixedLengthHeader {
/* 0 | 4 */ uint32_t HeaderSize;
/* XXX 4-byte hole */
/* 8 | 8 */ uint64_t CryptogramSize;
/* 16 | 1 */ uint8_t DevFlag;
/* 17 | 1 */ uint8_t HeaderVer;
/* XXX 2-byte hole */
/* 20 | 4 */ uint32_t PackagerVer;
/* 24 | 2 */ uint16_t Reserved;
/* XXX 6-byte padding */

/* total size (bytes): 32 */
}
(8+8+8)

/* offset | size */ type = struct FixedLengthHeader {
/* 0 | 4 */ uint32_t HeaderSize;
/* 4 | 1 */ uint8_t DevFlag;
/* 5 | 1 */ uint8_t HeaderVer;
/* XXX 2-byte hole */
/* 8 | 4 */ uint32_t PackagerVer;
/* 12 | 2 */ uint16_t Reserved;
/* XXX 2-byte padding */

/* total size (bytes): 16 */
}
(4+4+4+4)


没有添加#progma pack(4),编译指令:gcc -m32 -g test.c

/* offset | size */ type = struct FixedLengthHeader {
/* 0 | 4 */ uint32_t HeaderSize;
/* 4 | 8 */ uint64_t CryptogramSize;
/* 12 | 8 */ uint64_t ReservedField;

/* total size (bytes): 20 */
}
(4+(4+4)+(4+4))

/* offset | size */ type = struct FixedLengthHeader {
/* 0 | 4 */ uint32_t HeaderSize;
/* 4 | 8 */ uint64_t CryptogramSize;
/* 12 | 1 */ uint8_t DevFlag;
/* 13 | 1 */ uint8_t HeaderVer;
/* XXX 2-byte hole */
/* 16 | 4 */ uint32_t PackagerVer;
/* 20 | 2 */ uint16_t Reserved;
/* XXX 2-byte padding */

/* total size (bytes): 24 */
}
(4+(4+4)+4+4+4)


添加#progma pack(4),编译指令:gcc -g test.c

/* offset | size */ type = struct FixedLengthHeader {
/* 0 | 4 */ uint32_t HeaderSize;
/* 4 | 8 */ uint64_t CryptogramSize;
/* 12 | 8 */ uint64_t ReservedField;

/* total size (bytes): 20 */
}
(4+(4+4)+(4+4))

/* offset | size */ type = struct FixedLengthHeader {
/* 0 | 4 */ uint32_t HeaderSize;
/* 4 | 8 */ uint64_t CryptogramSize;
/* 12 | 1 */ uint8_t DevFlag;
/* 13 | 1 */ uint8_t HeaderVer;
/* XXX 2-byte hole */
/* 16 | 4 */ uint32_t PackagerVer;
/* 20 | 2 */ uint16_t Reserved;
/* XXX 2-byte padding */

/* total size (bytes): 24 */
}
(4+(4+4)+4+4+4)


添加#progma pack(16),编译指令:gcc -g test.c

/* offset | size */ type = struct FixedLengthHeader {
/* 0 | 4 */ uint32_t HeaderSize;
/* XXX 4-byte hole */
/* 8 | 8 */ uint64_t CryptogramSize;
/* 16 | 1 */ uint8_t DevFlag;
/* 17 | 1 */ uint8_t HeaderVer;
/* XXX 2-byte hole */
/* 20 | 4 */ uint32_t PackagerVer;
/* 24 | 2 */ uint16_t Reserved;
/* XXX 6-byte padding */

/* total size (bytes): 32 */
}
(8+8+8+8)
liuxu
2021-10-24 14:06:04 +08:00
还有一种情况,系统和编译器都是 64 位,编译目标编译出了 32 位和 64 位程序,debug 分析的时候选择了 32 位程序。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/809945

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX