清晰明白!一份内存对齐指南

129 阅读5分钟

1 简介

Go 语言以简洁和高性能著称,其中内存管理是它的重要组成部分。本文介绍了 Go 的内存对齐机制、平台差异、结构体布局优化、以及垃圾回收(GC)相关知识,并提供了一些优化建议。

image.png

2 不同系统下的内存表现(Windows vs Linux)

虽然 Go 的内存分配逻辑在所有平台一致,但底层地址表现会因操作系统而异,但底层地址表现可能会有差别,主要原因有:

	方面		    Linux		                Windows
	堆/栈起始地址	   通常是 0xc000xxxxxx 或者 0x140xxxxx    有时是 0x00xxxxx
	栈增长方向	       向下				 向下
	ASLR(地址随机化)    开启默认		    开启默认
	页大小		   通常 4KB		  通常 4KB
	Go 编译器行为	 几乎一致			 几乎一致

变量是否连续:大多数情况下连续,但不能保证。

地址是否变化:取决于编译器是否重排 + 是否逃逸到堆上。

地址是否一致:小程序因为堆/栈地址固定,地址可能一样;大程序或加上其他变量后,地址会变。

平台差异:底层地址可能不同,但不会影响语义或内存模型。

3 什么是内存对齐和内存对其填充

对于同一编译器,确切的类型对齐保证在不同体系结构之间和不同编译器版本之间可能有所不同。对于标准 Go 编译器的当前版本. 内存对齐是为了让数据按照它们类型需要的“边界”存储,从而提高 CPU 的读取效率。

        ```
        type  类型	          alignment guarantee	对齐字节数    
        ------                    ------
        bool, uint8, int8         1
        uint16, int16             2
        uint32, int32             4
        float32, complex64        4
        arrays                    depend on element types
        structs                   depend on field types
        other types               size of a native word
        ```

对齐填充(padding):Go 的栈会尽量保证变量地址按类型对齐,以满足 CPU 的要求(比如 int64 对齐到 8 字节边界);

  • 内存使用对齐的例子

我们使用相同字段,分别创建两个结构体属性分别为对齐或不对齐,帮助 go 更好地分配内存和 使用cpu读取,查看效果

	type RandomResource struct {
		Cloud               string // 16 bytes
		Name                string // 16 bytes
		HaveDSL             bool   //  1 byte
		PluginVersion       string // 16 bytes
		IsVersionControlled bool   //  1 byte
		TerraformVersion    string // 16 bytes
		ModuleVersionMajor  int32  //  4 bytes
	}

	type OrderResource struct {
		ModuleVersionMajor  int32  //  4 bytes
		HaveDSL             bool   //  1 byte
		IsVersionControlled bool   //  1 byte
		Cloud               string // 16 bytes
		Name                string // 16 bytes
		PluginVersion       string // 16 bytes
		TerraformVersion    string // 16 bytes

	}

结构体的字段存储使用的空间与顺序有关, 与字段值没有关系

		 var d RandomResource
		 d.Cloud = "aws-singapore"
		 ...

		 InfoHandler(fmt.Sprintf("随机顺序属性的结构体内存 总共占用 StructType: %T => [%d]\n", d, unsafe.Sizeof(d)), m)

		 var te = OrderResource{}
		 te.Cloud = "aws-singapore"  
		 ...
		 m.Logf("属性对齐的结构体内存 总共占用  StructType:d %T => [%d]\n", te, unsafe.Sizeof(te))

执行它,

	go test -v .\case_test.go

得到以下关键输出:

 随机顺序属性的结构体内存 总共占用 StructType: main.Raesource => [88]
				 
 属性对齐的结构体内存 总共占用  StructType:d main.OrderResource => [72]
  

也可以查看复制了对齐结构体后,并重新赋值,查看字段长度变换。

可以看到在结构体定义中,随机顺序比对齐后的结构体占用空间更多, 内存对齐的相同属性结构体go语言使用的内存可以更少。

  • 为什么顺序重要?

RandomResource:由于字段混合类型,Go 编译器需要添加填充(padding)来对齐字段,导致总占用 88 字节

OrderResource:字段合理排序后,占用 72 字节

这说明通过合理安排字段顺序,可以节省内存。

  • 为什么与结构体属性值无关?

在 Go 中,结构体字段的存储空间是固定的,与字段“值”的大小没有直接关系。

特别是像 stringslicemapinterface 这种“引用类型”,字段本身只存储了“指针信息”,而不是实际数据。

image.png

结构体字段存储大小和存储位置如下

            结构体字段类型  值越长占用越大?  字段本身大小	实际数据存储
            intbool 等	否	    固定(如 48)	在字段本身
             string	         否	     固定(16 字节)	在堆上
              slice	         否	     固定(24 字节)	底层数组在堆上
            map/interface	 否	     固定(16 字节)	实际内容在堆上

可以看到“结构体字段大小与字段值无关”(尤其针对引用类型)。但值越大,结构体本身之外的内存消耗也会更大。

4 小结

对于整数,go一般按顺序8字节存放,然而对于一个结构体而言,结构体属性为随机顺序的,本体和复制后,go都将分配更多内存空间。

比如 结构体的Cloud 字段。Sizeof表达式大小总是16,而对齐系数 Alignof 大小总是8,在不同的结构体实例中值长度可以为 4,13, 105.

使用 unsafe.Sizeof() 和 unsafe.Alignof() 可以检测字段占用空间和对齐方式。 测试结果表明:

    结构体 存储使用的空间 **与字段值没有关系**
    string 类型变量在结构体中占用 固定16字节(即指针+长度)。
    但结构体值的实际存储长度可以变化,如 4、13、105 等。

    复制结构体时,值会拷贝,但指针地址不会变,除非使用 & 引用。

在本文我们介绍了go的整数内存管理方式,以及如何判断定义的变量是否被顺序整齐存放,并介绍unsafe包的检查功能,在初始化时,go结构体已经分配了对应的的内存空间,并在最后介绍了go语言的内存管理优点和常见的优化思路。