V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mingl0280
V2EX  ›  C++

问个关于内存对齐的问题

  •  
  •   mingl0280 · 45 天前 · 1939 次点击
    这是一个创建于 45 天前的主题,其中的信息可能已经有所发展或是发生改变。

    为啥

    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)的内存呢……

    20 条回复    2021-10-24 14:06:04 +08:00
    secondwtq
        1
    secondwtq  
       45 天前   ❤️ 1
    你这个 FixedLengthHeader 在我这是 24 啊 ...

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

    https://godbolt.org/z/c1TM4778j
    secondwtq
        6
    secondwtq  
       45 天前
    我猜可能哪个 header 里面 #pragma pack push 了忘 pop 了 ... 你可以 -E 一下看看。实在不行一点点 reduce,能 reduce 到放在 godbolt 上面的程度更好

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

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

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

    或者:
    #pragma pack(push)
    #pragma pack(n)
    #pragma pop()
    elfive
        11
    elfive  
       45 天前 via iPhone
    @elfive #10 最后那个#pragma pop 写错了,应该是#pragma pack(pop)
    mingl0280
        12
    mingl0280  
    OP
       45 天前 via Android
    @secondwtq 生成出来的二进制文件我按着字节看的,第一个 int 是 0x14,真就是 20 字节。看到的时候人都给我搞懵逼了……
    mingl0280
        13
    mingl0280  
    OP
       45 天前 via Android
    @secondwtq 该文件引用的头文件中(只有一个十来行的自定义头文件)并没有其他的 pack 指令(其它头文件都是标准库),我挨个核对过
    @elfive
    NoAnyLove
        14
    NoAnyLove  
       45 天前   ❤️ 1
    没有用紧凑声明的话就会默认 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
        15
    jones2000  
       45 天前
    强制 1 字节对齐
    westtide
        16
    westtide  
       44 天前
    ABI 规范只定义了变址的 对齐方式,并没有定义变蜇的分配顺序 编译器可以自由决定使用何种顺序来分配变量 。
    对于由基本数据类型构造而成的 struct 结构体数据,为了保证其中每个成员都满足对齐要 求,i386 System V ABI 对 strucl 结构体数据的对齐方式有如下几条规则:
    整个结构体变显的 对齐方式与其中对齐方式最严格的成员相同;
    每个成员在满足其对齐方式的前提下,取最小的可用位置作为成员在结构体中的偏移址,这可能导致内部插空;
    结构体大小应为对齐 边界长度的整数倍,这可能导致尾部插空 。

    《计算机系统基础 第 2 版 袁春风余子濠 © 编著》
    liuxu
        17
    liuxu  
       44 天前
    你下面的不是
    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
        18
    bigHentai  
       44 天前
    我记得默认是按结构体中最大那个变量的字节数对齐,所以是 uint64_t,也就是 8 字节对齐,上面那个默认应该是 24 ,下面的应该是 32 ,你应该是开了强制 4 字节对齐之类的?
    liuxu
        19
    liuxu  
       43 天前
    @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
        20
    liuxu  
       43 天前
    还有一种情况,系统和编译器都是 64 位,编译目标编译出了 32 位和 64 位程序,debug 分析的时候选择了 32 位程序。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   4032 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 02:58 · PVG 10:58 · LAX 18:58 · JFK 21:58
    ♥ Do have faith in what you're doing.