V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
gps32251070
V2EX  ›  程序员

关于编程语言内存对齐的疑问

  •  
  •   gps32251070 · 2022-08-14 18:40:36 +08:00 · 3404 次点击
    这是一个创建于 857 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近对于编程语言为什么要进行内存对齐有些疑问,网上看的资料基本都是说为了 CPU 的效率,减少 CPU 访问内存的次数,但是总感觉这种说法很勉强,举例: 对于 64 位系统,CPU 按 8 字节取内存,那么假设有

    struct s { a int16 b int64 }

    对于这个结构体,我的理解是不管对不对齐 CPU 总是要取两次的,如果这个时候多了个 c 变量

    struct s { a int16 b int64 c int16 }

    如果不对齐的话,那么只需要 2+8+2=12 个字节空间,CPU 只需要取两次。如果是内存对齐,那么 需要 8+8+8=24 个字节空间,CPU 反而需要读三次,这么来看不对齐不但节省 CPU 时间,还节省内存空间,所以为什么要对齐呢?。。。难道 CPU 拼接变量很消耗时间?

    23 条回复    2022-08-15 14:34:48 +08:00
    LaTero
        1
    LaTero  
       2022-08-14 18:52:09 +08:00 via Android
    你下面这个结构有问题,要对齐的话是在 a 和 b 之间补 6 个字节,这样 b 在 8 个字节的整数位上。而补进去的 padding cpu 根本就不用,为什么会读三次?
    补齐之后要读 b 只需要读一次,而你上面这个结构要读 b 的话,x86 架构会读两次再拼起来。而很多架构是没有这个功能的,直接 panic
    across
        2
    across  
       2022-08-14 18:57:32 +08:00
    因为你在两个不同场景下进行比较。一个数据量多一个数据量少。 要排布优化你自己不能调下顺序么···
    Jooooooooo
        3
    Jooooooooo  
       2022-08-14 19:00:05 +08:00
    你要不多搜搜资料

    另外具体到第二个例子, int16 和 int16 会被放在一起的
    gps32251070
        4
    gps32251070  
    OP
       2022-08-14 19:01:29 +08:00 via iPhone
    @LaTero 我说的读三次是读完整个结构体的次数。至于拼接,难道是现在大部分 Cpu 都没有这些指令?如果是这样就可以说得过去了。
    gps32251070
        5
    gps32251070  
    OP
       2022-08-14 19:05:36 +08:00 via iPhone
    @Jooooooooo 我知道为了节省内存空间要放在一起,但是不放到一起会导致内存多用的原因是需要内存对齐,我只是对为什么需要对齐有疑问
    charslee013
        6
    charslee013  
       2022-08-14 19:08:03 +08:00
    首先误解了对齐的操作对象了,操作对象是结构体里面的字段,而不是整个结构体

    比如 struct s { a int16 b int64 c int16 } ,如果是 2+8+2 的方式,操作 b 字段需要两个内存操作
    因为 b 分在两个 8 字节内存块中了
    而对齐之后的 8+8+8 只需要一个内存操作就能操作任意一个字段

    > 那么如果不对齐会怎么样?
    不会怎么样,无论数据是否对齐在 x86-64 硬件都能正确工作
    hsfzxjy
        7
    hsfzxjy  
       2022-08-14 19:08:05 +08:00 via Android
    ryd994
        8
    ryd994  
       2022-08-14 19:08:55 +08:00   ❤️ 2
    x86 架构对于对齐的内存,写入读取都是原子的(但自增运算需要用专门的指令,另说)
    其他一些架构甚至不支持非对齐内存的原子操作。
    你搜一下 unaligned access 就可以搜到很多内容了。

    “不管对不对齐 CPU 总是要取两次的”
    不对齐的情况下可能需要三倍的开销,除非编译器优化。
    访问 a 需要一次,访问 b 可能需要两倍的时间

    "如果不对齐的话,那么只需要 2+8+2=12 个字节空间,CPU 只需要取两次"
    同上。如果不对齐的话,结果是取 2 ,然后 6+2 ,再 2 。

    struct 内部重排以减少不必要的 padding ,这是性能优化的基础技巧之一。
    一般我们会把 64 位变量放前面,然后 32 位,然后 16 位。因为 64 位对齐一定同时也是 32 位和 16 位对齐,反之未必。

    编译器不会对 struct 内的顺序进行重排,因为有些操作可能会默认各个变量之间的顺序。
    ryd994
        9
    ryd994  
       2022-08-14 19:12:36 +08:00
    @charslee013 “首先误解了对齐的操作对象了,操作对象是结构体里面的字段,而不是整个结构体”
    这一点上并没有错,struct 里的字段要对齐,整个 struct 的大小也需要对齐。因为创建 array 的时候,如果 struct 大小没有补足的话,那第二个元素就对不齐了。
    你可以吧这个 struct 实际编译一下,看看 sizeof 是不是补足到 pack size 了。
    across
        10
    across  
       2022-08-14 19:20:26 +08:00
    想了下题主的问题应该是:
    为什么和内存没对齐相比,cpu 处理对齐的速度要快一点?没对齐的会多出哪些操作?

    因为这个和 cpu 总线、寄存器、内存结构有关,唔···这就是长篇了,现在不敢保证我细节都能说对。
    因为总线、寄存器本身有个大小,假如寄存器 B 64 位,总线 64 位,那数据就是 64 位批量取的,cpu 就是这么个寻址方式(关于为什么这样寻址,就要写很长了), 空间没对齐,cpu 确实需要额外拼接,这个耗时间。
    des
        11
    des  
       2022-08-14 19:37:15 +08:00 via iPhone
    想象一下地砖的格子,CPU 一次性是取“一个格子”的数据,如果你的数据正好跨了两格,cpu 自然是需要操作多次,并且把数据拼接起来。
    icyalala
        12
    icyalala  
       2022-08-14 19:37:45 +08:00
    Linux Kernel 里的文章:
    https://github.com/torvalds/linux/blob/master/Documentation/core-api/unaligned-memory-access.rst

    但实际来说,最新的一些 x86 处理器实际是支持未对齐内存访问的,而且也可以认为没有性能下降。
    旧一些的 x86 处理器也支持,但是会有性能下降。当然指令还是那个指令。
    其他的要看具体 arch 支持程度了,不支持的话甚至会出现 misaligned access 异常。
    secondwtq
        13
    secondwtq  
       2022-08-14 19:44:22 +08:00   ❤️ 1
    第一,进行内存对齐的一般是编程语言的*实现*,不是编程语言

    然后,就 x86 来说,一般编程语言的实现取 a ,b ,c 的方法是 mov ax, [s]; mov rbx, [a+8]; mov cx, [a+16],按照你那种紧凑的布局无非就是变成了 mov ax, [s]; mov rbx, [a+2]; mov cx, [a+10],都是三次
    也就是说一般根本不会先整个 word size 读过来再拼接,拼来拼去的做法在 SIMD 里倒是比较常见

    就算按照楼主的说法,不对齐,先取,再拼,省了一个 load ,多了几个位运算,不一定划算
    楼主可能认为 load 很 costly ,其实大多数 load 都还好,只有 cache miss 的 load 才 costly

    现代 x86 实现里面,非对齐的访问一般是不会有性能损失的,但是仅限于在一个 cache line 里面,如果跨了 cache line 就相当于 CPU 要帮你自动做两次+拼接,要是跨了页就更好玩了。对于在 L1D$里的数据,在对齐的情况下,每次 load 的延迟和占用的资源基本都是确定且最小的,而如果出现了跨 cacheline 或跨页,就会出现有些 load 和对齐的没区别,有些 load 则非常慢的情况,平均下来是降低了性能的
    这个在 GPR 操作上影响还算小的,如果涉及到 SIMD ,连续 load 一串数据,对于 XMM load ,四分之一会出现跨 cache line ,对于 YMM 是二分之一,对于 ZMM 是百分百 ...

    有没有需要紧凑布局的情况呢?当然也有,就是真的需要“节省内存空间”的时候,比如大量并行+数据量大的情况下如果你的算法不能优化到 cache 里面,DRAM 喂不饱 CPU 很正常,这时需要尽量利用内存带宽,而 ALU 运算就基本无所谓了,不仅 padding 可以不用,bitfield 也可以用上
    secondwtq
        14
    secondwtq  
       2022-08-14 20:12:24 +08:00
    上面是 load ,不知道楼主打算怎么做 store 。现代 CPU 中单独的 store 指令比 load 更 cheap ,因为只需要往 store buffer 里面压一压,不会造成新的依赖。
    按照拼的思路,你得做两次 load+两次 store ,本来一个 store 解决的事情,至于么 ...
    直接存的话有和 load 一样的问题

    另外根据 https://travisdowns.github.io/blog/2019/06/11/speed-limits.html#load-split-cache-lines ,在 Zen 系列上不仅跨 64 byte 边界的访问会影响性能,跨 32 byte 也有可能
    hotyogurt
        15
    hotyogurt  
       2022-08-14 20:41:24 +08:00
    @des #11 你好,请教一下为何 CPU 取数据是一个个格子读的?也就是为什么读内存数据只能从对齐的地址开始?谢谢。
    des
        16
    des  
       2022-08-14 21:20:18 +08:00   ❤️ 1
    @hotyogurt 这和 CPU 的设计有关系,主要可以得到一些好处,可以自己看
    https://stackoverflow.com/questions/3025125/cpu-and-data-alignment
    jaynos
        17
    jaynos  
       2022-08-14 21:27:22 +08:00
    话说可以参考下这个视频: https://www.bilibili.com/video/BV1hv411x7we
    root111
        18
    root111  
       2022-08-14 21:34:55 +08:00
    @secondwtq 你好,请教下,出现跨 cache line 或 page ,非对齐的 load 的开销具体在哪?
    FrankHB
        19
    FrankHB  
       2022-08-14 23:51:25 +08:00   ❤️ 2
    你对实现机制理解严重不足。
    对齐的直接对象是处理器访存指令中的地址操作数。访存要对齐,根本原因不对齐的地址需要额外的计算而不划算。假定对齐的地址可以直接当做低几位是 0 。
    对 CISC 处理器,硬件可能加更多电路以确保地址的每一位都有效(有时还得检查是否对齐引发异常),而假定对齐的地址访问直接就把低几位忽略了。
    RISC 设计甚至就基本把不对齐访问给省了,ISA 层面上不支持不对齐访问,真不对齐可能就直接异常(并行的,原则上正常路径不耗时间)。如果你要强行非对齐访存,那么就得用粒度更小、延迟可能更大的特设访存指令,或者访存完再截取一段数据这种软件方式模拟,这些都是开销更大的,差一个数量级都正常。
    这个意义下,同样一次逻辑意义上的访存,两者的开销本来就不保证一样大(就算同时支持对齐和不对齐访存,非对齐的访问可能更耗指令周期;虽然也设计有一样的,但一般至少不能反过来指望不对齐更快)。
    FrankHB
        20
    FrankHB  
       2022-08-15 00:29:49 +08:00
    @root111 总体原因是局域性。
    即便只是核内的第一级缓存,cache 和执行访存的实现电路(比如 LSU )不是一个部件,要操作 cache 物理上必须发信号等待同步,确保满足 cache coherency 以保证之后 cache 的状态可预测。只要不是允许禁用缓存这个开销就无法避免,但后面几级缓存不确定性就大了,比如都 hit 就很快,反之要跟后一级缓存直至主存同步,相比就慢得多。
    现在的级联 cache 设计的关联策略可以保证前级 cache 如果只操作同一个 cache line ,后级 cache 也可以在同一个 cache line (如果只是 load 都 hit 就可以不管后级 cache ),反过来难以保证。所以一旦跨 cache line ,脸不好就引发刷后级 cache 直至刷到 uncore 里的 LLC 甚至主存的最慢的路径,差距很大。另外,如果占用多个 cache line ,意味着其它数据能占用的 cache 就少了(也更容易刷出去),会全局地阻碍 cache 的加速作用。
    跨 page 涉及到的东西就更多。page 是主存提供主要空间的地址空间里的结构,现代机器基本都是 MMU 实现的,里面有一些专用寄存器帮助实现 page table 、TLB 之类一大坨数据结构,具体补课体系结构和操作系统。跨 page 的访问基本上是得拆到两个 page 的,以反应不同 page 允许具有的不同状态。同一个 page 内硬件可能按历史访存请求以减少访问这些数据结构的开销,但跨 page 这些就大半失效了。
    当然不管是不是跨都可能有物理内存没加载就绪的情况 page fault 了。基本如果要能用也是得操作系统一大坨软件代码分配空间,不能用也是不常见的慢速中断路径,要快就见鬼了。这里跨 page 可能一次性开销×2 。
    FrankHB
        21
    FrankHB  
       2022-08-15 00:46:29 +08:00
    补:关于填 padding 还有避免 cache line 上 false sharing 的问题,某种意义上也是对齐问题但跟一般访存对齐不同,不过这个就更依赖实现细节,真要优化可能得分析具体的一致性协议。
    cf. zhuanlan.zhihu.com/p/374586744
    SunBK201
        22
    SunBK201  
       2022-08-15 02:25:31 +08:00 via iPad   ❤️ 1
    简单来说,因为访存地址是”对齐”的( CPU 访存时会以 offset 的形式给出地址,并不是 Byte 的形式),一次访存能整读整个 offset 行, 这样一次读一个 offset 的做法可以节省 CPU 访存地址总线,当然要以非对齐的形式读也是可以实现的,可以通过增加 CPU 的地址总线实现, 不过过多的引脚会增加 CPU 的复杂度。
    cubecube
        23
    cubecube  
       2022-08-15 14:34:48 +08:00
    对齐还和 cache line 有关系呢,多查资料学习。
    c struct 不会自动对齐,可以自己写几个测试程序跑跑对比下
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1037 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 19:11 · PVG 03:11 · LAX 11:11 · JFK 14:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.