Skip to content

Latest commit

 

History

History
186 lines (118 loc) · 9.98 KB

data_alignment.md

File metadata and controls

186 lines (118 loc) · 9.98 KB

内存对齐

内存对齐是编译器的管辖范围。表现为:编译器为程序中的每个“数据单元”安排在适当的位置上。

为什么需要内存对齐?

  1. 有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可以移植性了。

  2. CPU 访问内存时并不是逐个字节访问,而是以字长(word size)为单位访问,例如 32位的CPU 字长是4字节,64位的是8字节。如果变量的地址没有对齐,可能需要多次访问才能完整读取到变量内容,而对齐后可能就只需要一次内存访问,因此内存对齐可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量。

假设每次访问的步长为4个字节,如果未经过内存对齐,获取b的数据需要进行两次内存访问,最后再进行数据整理得到b的完整数据:

如果经过内存对齐,一次内存访问就能得到b的完整数据,减少了一次内存访问:

默认对齐系数

不同平台的默认对齐系数一般不同的,一般如下规则:

  • 32位平台对齐系数是4
  • 64位平台对齐系数是8

Go语言中的对齐系数

unsafe.AlignOf(x) 方法的返回值是 m,当变量进行内存对齐时,需要保证分配到 x 的内存地址能够整除 m。m就是变量x的对齐数,需要注意的是对齐系数m一定不会超过平台的默认对齐系数。

确定变量x在内存中对齐系数,规则如下:

  • 对于任意类型的变量 x ,unsafe.Alignof(x) 至少为 1。
  • 对于 struct 结构体类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f),unsafe.Alignof(x) 等于其中的最大值。
  • 对于 array 数组类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐倍数。

内存对齐规则:

  • 成员对齐规则

针对一个基础类型变量,如果 unsafe.AlignOf() 返回的值是 m,那么该变量的地址需要 被m整除 (如果当前地址不能整除,填充空白字节,直至可以整除)。

  • 整体对齐规则

针对一个结构体,如果 unsafe.AlignOf() 返回值是 m,需要保证该结构体整体内存占用是 m的整数倍,如果当前不是整数倍,需要在后面填充空白字节。

通过内存对齐后,就可以在保证在访问一个变量地址时:

  • 如果该变量占用内存小于字长:保证一次访问就能得到数据;
  • 如果该变量占用内存大于字长:保证第一次内存访问的首地址,是该变量的首地址。

示例1

type A struct {
    a int32
    b int64
    c int32
}

func main() {
    fmt.Println(unsafe.Sizeof(A{1, 1, 1}))  // 24
}

第一个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,我们假设地址从0开始,0可以被4整除:

第二个字段是 int64 类型,unsafe.Sizeof(int64(1)) = 8,内存占用为 8 个字节,同时unsafe.Alignof(int64(1)) = 8,需保证变量放置首地址可以被8整除,当前地址为4,距离4最近的且可以被8整除的地址为8,因此需要添加四个空白字节,从8开始放置:

第三个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,当前地址为16,16可以被4整除:

所有成员对齐都已经完成,现在我们需要看一下整体对齐规则:unsafe.Alignof(A{}) = 8,即三个变量成员的最大值,内存对齐需要保证该结构体的内存占用是 8 的整数倍,当前内存占用是 20个字节,因此需要再补充4个字节:

最终该结构体的内存占用为 24字节。

示例2

type B struct {
    a int32
    b int32
    c int64
}

func main() {
    fmt.Println(unsafe.Sizeof(B{1, 1, 1}))  // 16
}

第一个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,我们假设地址从0开始,0可以被4整除:

第二个字段是 int32 类型,unsafe.Sizeof(int32(1))=4,内存占用为4个字节,同时unsafe.Alignof(int32(1)) = 4,内存对齐需保证变量首地址可以被4整除,当前地址为4,4可以被4整除:

第三个字段是 int64 类型,unsafe.Sizeof(int64(1))=8,内存占用为8个字节,同时unsafe.Alignof(int64(1)) = 8,内存对齐需保证变量首地址可以被8整除,当前地址为8,8可以被8整除:

所有成员对齐都已经完成,现在我们需要看一下整体对齐规则:unsafe.Alignof(B{}) = 8,即三个变量成员的最大值,内存对齐需要保证该结构体的内存占用是 8 的整数倍,当前内存占用是 16个字节,已经符合规则,最终该结构体的内存占用为 16个字节。

空结构体的对齐规则

如果空结构体作为结构体的内置字段:当变量位于结构体的前面和中间时,不会占用内存;当该变量位于结构体的末尾位置时,需要进行内存对齐,内存占用大小和前一个变量的大小保持一致。

type C struct {
 a struct{}
 b int64
 c int64
}

type D struct {
 a int64
 b struct{}
 c int64
}

type E struct {
 a int64
 b int64
 c struct{}
}

type F struct {
 a int32
 b int32
 c struct{}
}

func main() {
 fmt.Println(unsafe.Sizeof(C{})) // 16
 fmt.Println(unsafe.Sizeof(D{})) // 16
 fmt.Println(unsafe.Sizeof(E{})) // 24
 fmt.Println(unsafe.Sizeof(F{})) // 12
}

《Linux系统编程》一书中关于对齐的介绍

对齐

数据对齐(alignment)是指数据在内存中的存储排列方式。如果内存地址A是2的n次幂的整数倍,我们就说A是n字节对齐。处理器、内存子系统以及系统中的其他组件都有特定的对齐需求。举个例子,大多数处理器的工作单位是字,只能访问字对齐的内存地址。同样,正如前面所讨论的,内存管理单元也只处理页对齐的地址。

如果一个变量的内存地址是它大小的整数倍时,就称为“自然对齐(naturally aligned)”。例如,对于一个32位长的变量,如果它的地址是4(字节)的整数倍(也就是说,如果地址的低两位是0),那就是自然对齐了。因此,如果一个类型的大小是2n字节,那么它的内存地址至少低n位是0。

数据对齐的规则是依赖于硬件的,因此不同系统的对齐规则不同。有些体系的计算机在数据对齐方面有很严格的要求,而有的很松散。当数据不对齐时,有的系统会生成一个可捕捉的错误。内核可以选择终止该进程或(更多情况下是)手工处理没有对齐的访问(通常通过多个对齐访问完成)。这种处理方式会引起性能下降,但至少进程不会终止。在编写可移植的代码的时候,编程人员一定要注意不要破坏了数据对齐规则。

预对齐内存的分配

在大多数情况下,编译器和C库会自动处理对齐问题。POSIX规定通过malloc()、calloc()和realloc()返回的内存空间对于C中的标准类型都应该是对齐的。在Linux中,这些函数返回的地址在32位系统是以8字节为边界对齐,在64位系统是以16字节为边界对齐的。

其他对齐问题

对齐问题并不局限于标准类型与动态内存分配的自然对齐。比如说,非标准的和复杂的数据类型的对齐问题会比标准类型(比如int类型)的更复杂。

非标准和复杂的数据类型的对齐比简单的自然对齐有更多的要求,可以遵循以下四条规则:

  • 结构体的对齐要求和它的成员中最大的那个类型是一样的。例如,一个结构中最大的是以4字节对齐的32bit的整型,那么这个结构至少以4字节对齐。
  • 结构体也带来了填充问题,以此来保证每一个成员都符合各自的对齐要求。因此,如果一个char(很可能是以1字节对齐)后跟着一个int(很可能是以4字节对齐),编译器会自动地插入3个字节作为填充来保证int以4字节对齐。编程人员有时需要注意一下结构体中成员变量的顺序,比如按成员变量类型大小降序来定义它们,从而减少由于填充所带来的空间浪费。使用GCC编译时,加入-Wpadded 选项可以帮助你实现这个优化,当编译器隐式填充时,它会发出警告。
  • 一个联合类型的对齐和联合类型里类型大小最大的一致。
  • 一个数组的对齐和数组里的基本元素类型一致。因此,除了对数组元素的类型做对齐外,数组没有其他的对齐需求。这样可以使数组里面的所有成员都是自然对齐的。

总结

  • unsafe.Sizeof(x) 返回了变量x的内存占用大小
  • 两个结构体,即使包含变量类型的数量相同,但是位置不同,占用的内存大小也不同,由此引出了内存对齐
  • 内存对齐包含成员对齐和整体对齐,与 unsafe.AlignOf(x) 息息相关
  • 空结构体作为成员变量时,是否占用内存和所处位置有关
  • 在实际开发中,我们可以通过调整变量位置,优化内存占用(一般按照变量内存大小顺序排列,整体占用内存更小)

来源