由于数据是通过总线进行传输,若数据未经一定规则的对齐,CPU的访址操作与总线的传输操作将会异常的复杂,所以编译器在程序编译期间会对各种类型的数据按照一定的规则进行对齐。
例如: 现在要存储变量A(int32)和B(int64)那么不做任何字节对齐优化的情况下,内存布局是这样的
字节对齐优化后是这样子的:
一看感觉字节对齐后浪费了内存, 但是当我们去读取内存中的数据给CPU时,64位的机器(一次可以原子读取8字节)在内存对齐和不对齐的情况下A变量都只需要原子读取一次就行, 但是对齐后B变量的读取只需一次, 而不对齐的情况下,B需要读取2次,且需要额外的处理牺牲性能来保证2次读取的原子性。所以本质上,内存填充是一种以空间换时间, 通过额外的内存填充来提高内存读取的效率的手段。
总的来说,内存对齐主要解决以下两个问题:
【1】跨平台问题:如果数据不对齐,那么在64位字长机器存储的数据可能在32位字长的机器可能就无法正常的读取。
【2】性能问题:如果不对齐,那么每个数据要通过多少次总线传输是未知的,如果每次都要处理这些复杂的情况,那么数据的读/写性能将会收到很大的影响。之所以有些CPU支持访问任意地址是因为它多做了很多处理,是因为处理器在后面多做了很多额外处理。
现代处理器原子操作的实现原理: 1. 处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的. 2. 复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。 3. 实际上大多数原子操作的保证都在硬件层面提供了指令支持,编程语言(C, C++, Go, Java等)无非就是封装一层来保证在不同类型的处理器上能够正确地调用对应的指令。
内存对齐主要是为了保证数据的原子读取, 因此内存对齐的最大边界只可能为当前机器的字长。当然如果每种类型都使用最大的对齐边界,那么对内存将是一种浪费,实际上我们只要保证同一个数据不要分开在多次总线事务中便可。
Go在其官方文档 Size and alignment guarantees - golang spec 就描述了其在内存对齐方面的细节。
Go也提供了unsafe.Alignof(x)来返回一个类型的对齐值,并且作出了如下约定:
总结来说,分为基本类型对齐和结构体类型对齐
数据类型 | 类型大小(32/64位) | 最大对齐边界(32位) | 最大对齐边界(64位) |
---|---|---|---|
int8/uint8/byte | 1字节 | 1 | 1 |
int16/uint16 | 2字节 | 2 | 2 |
int32/uint32/rune/float32/complex32 | 4字节 | 4 | 4 |
int64/uint64/float64/complex64 | 8字节 | 4 | 8 |
string | 8字节/16字节 | 4 | 8 |
slice | 12字节/24字节 | 4 | 8 |
我们可以在自己的机器上编码测试了一下(我的机器是64位的 Mac OS X):
package service import ( "testing" "unsafe" ) func TestAlign(t *testing.T) { var byteTest byte = 'a' var int8Test int8 = 0 var int16Test int16 = 0 var int32Test int32 = 0 var int64Test int64 = 0 var uint8Test uint8 = 0 var uint16Test uint16 = 0 var uint32Test uint32 = 0 var uint64Test uint64 = 0 var float32Test float32 = 0.0 var float64Test float64 = 0.0 println("byte max align size =>", unsafe.Alignof(byteTest)) println("int8/uint8 max align size =>", unsafe.Alignof(int8Test), "/", unsafe.Alignof(uint8Test)) println("int16/uint16 max align size =>", unsafe.Alignof(int16Test), "/", unsafe.Alignof(uint16Test)) println("int32/uint32/float32 max align size =>", unsafe.Alignof(int32Test), "/", unsafe.Alignof(uint32Test), "/", unsafe.Alignof(float32Test)) println("int64/uint64/float64 max align size =>", unsafe.Alignof(int64Test), "/", unsafe.Alignof(uint64Test), "/", unsafe.Alignof(float64Test)) var s string = "343240000000000" println("string max align size =>", unsafe.Alignof(s)) var sliceTest []string println("slice's size/max align size =>", unsafe.Alignof(sliceTest), "/", unsafe.Sizeof(sliceTest)) var structTest struct{} println("struct{}'s size / max align size =>", unsafe.Alignof(structTest), "/", unsafe.Sizeof(structTest)) }
运行结果:
byte max align size => 1 int8/uint8 max align size => 1 / 1 int16/uint16 max align size => 2 / 2 int32/uint32/float32 max align size => 4 / 4 / 4 int64/uint64/float64 max align size => 8 / 8 / 8 string max align size => 8 slice's size/max align size => 8 / 24 struct{}'s size / max align size => 1 / 0
下面通过一些列的例子来说明一下结构体对齐的规则,需要额外说明的是结构体内的字段位置其实都是通过计算到结构体首地址的偏移量来确定的,对所有的字段来说,首地址就是结构体内索引值为0的地址。
案例一
type TestStruct1 struct { a int8 // 1 字节====> max align 1 字节 b int32 // 4 字节====> max align 4 字节 c []string // 24 字节====> max align 8 字节 }
TestStruct1在编译期就会进行字节对齐的优化。优化后各个变量的相对位置如下图(以64位字长下环境为例):
TestStruct1 内存占用大小分析:最大对齐边界为8,总体字节数 = 1 + (align 3) + 4 + 24 = 32, 由于32刚好是8的倍数,所以末尾无需额外填充,最后这个结构体的大小为32字节。
案例二
type TestStruct2 struct { a []string // 24 字节====> max align 8 字节 b int64 // 8 字节====> max align 8 字节 c int32 // 4 字节====> max align 4 字节 }
TestStruct2 内存占用大小分析:最大对齐边界为8字节,总体字节数 = 24(a) + 8(b) + 4(c) + 4(填充) = 40, 由于40刚好是8的倍数,所以c字段填充完后无需额外填充了。
案例三
type TestStruct3 struct { a int8 b int64 c struct{} }
TestStruct3 内存占用大小分析:最大对齐边界为8字节,总体字节数 = 1(a)+ 7(填充) + 8(b) + 8(c填充)=24, 空结构体理论上不占字节,但是如果在另一个结构体尾部则需要进行额外字节对齐 。
案例四
type TestStruct4 struct { c struct{} a int8 b int32 }
TestStruct4 内存占用大小分析:最大对齐边界为4字节,总体字节数 = 0(a)+ 1(b)+ 7(填充) + 4(c) = 8。
执行以下代码(环境是64位机器字长的)就可以看到我们之前案例中分析的结果。
func TestAlignStruct(t *testing.T) { var testStruct1 TestStruct1 println("size of testStruct1:", unsafe.Sizeof(testStruct1)) var testStruct2 TestStruct2 println("size of testStruct2:", unsafe.Sizeof(testStruct2)) var testStruct3 TestStruct3 println("size of testStruct4 / testStruct4's a size:", unsafe.Sizeof(testStruct3), "/", unsafe.Sizeof(testStruct3.c)) var testStruct4 TestStruct4 println("size of testStruct4 / testStruct4's a size:", unsafe.Sizeof(testStruct4), "/", unsafe.Sizeof(testStruct4.a)) }
输出为:
=== RUN TestAlignStruct size of testStruct1: 32 size of testStruct2: 40 size of testStruct4 / testStruct4's a size: 24 / 0 size of testStruct4 / testStruct4's a size: 8 / 0 --- PASS: TestAlignStruct (0.00s) PASS
关于golang内存对齐就介绍到这里了,有兴趣的记得点赞哦!
3月24日,腾讯发布2020年Q4及全年财报,其中金融科技及企业服务第四季收入385亿...
基于阿里巴巴的互联网架构、大数据技术,利用混合云架构打造全新的云化电子税 务...
1.某女生寝室门口贴着一个告示男生与饭盒不得入内,问何解?答曰两者都会搞大女...
作者 | 楚奕 来源 | 阿里技术公众号 这篇文章主要从技术视角介绍下跨平台WebCanv...
1.在报名的路上,我看见远处的学校,轰!的一声没了。希望如此。 2.男:我一直...
创业与投资的本质,都是追寻一种能够穿越时空,抵达未来的高效方式。 德勤管理咨...
1.百度是个大骗子,我抄了十几年的满分作文却从未得过满分。 2.学神在刷难题,...
前言 微服务成了互联网架构的标配模式,对微服务之间的调用的流量治理和管控就尤...
本文转载自微信公众号「后端Q」,作者conan。转载本文请联系后端Q公众号。 概述 ...
背景 有时候我会碰到快速搭建测试服务的需求,比如像这样: 搭建一个 HTTP Servi...