前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Golang中的内存对齐

Golang中的内存对齐

原创
作者头像
Johns
修改于 2022-06-30 02:32:39
修改于 2022-06-30 02:32:39
4.4K0
举报
文章被收录于专栏:代码工具代码工具

一. 什么是内存对齐, 为啥要内存对齐?

在解释什么是内存对齐之前,我们需要先了解一下CPU和内存数据交互的过程。CPU和内存是通过总线进行数据交互的。其中地址总线用来传递CPU需要的数据地址,内存将数据通过数据总线传递给CPU, 或者CPU将数据通过数据总线回传给内存。
image.png
image.png

首先我们需要知道以下概念:

(1) 机器字长

在计算机领域,对于某种特定的计算机设计而言,(word)是用于表示其自然的数据单位的术语,是用来表示一次性处理事务的固定长度。一个字的位数,即字长

(2). 地址总线

专门用来传送地址的,由于地址只能从CPU传向外部存储器或I/O端口,所以地址总线总是单向的。地址总线的位数决定了CPU可直接寻址的内存空间大小,比如8位微型机的地址总线为16位,则其最大可寻址空间为2^16=64KB,16位微型机的地址总线为20位,其可寻址空间为2^20=1MB。

(3). 数据总线

是CPU与内存或其他器件之间的数据传送的通道。每条传输线一次只能传输1位二进制数据, 数据总线每次可以传输的字节总数就称为机器字长或者数据总线的宽度。 它决定了CPU和外界的数据传送速度。我们现在日常使用的基本上是32位(每次可以传输4字节)或者64位(每次可以传输8字节)机器字长的机器。

由于数据是通过总线进行传输,若数据未经一定规则的对齐,CPU的访址操作与总线的传输操作将会异常的复杂,所以编译器在程序编译期间会对各种类型的数据按照一定的规则进行对齐, 对齐过程会按一定规则对内存的数据段进行的字节填充, 这就是字节对齐

例如: 现在要存储变量A(int32)和B(int64)那么不做任何字节对齐优化的情况下,内存布局是这样的

字节不对齐
字节不对齐

字节对齐优化后是这样子的:

字节对齐.png
字节对齐.png

一看感觉字节对齐后浪费了内存, 但是当我们去读取内存中的数据给CPU时,64位的机器(一次可以原子读取8字节)在内存对齐和不对齐的情况下A变量都只需要原子读取一次就行, 但是对齐后B变量的读取只需一次, 而不对齐的情况下,B需要读取2次,且需要额外的处理牺牲性能来保证2次读取的原子性。所以本质上,内存填充是一种以空间换时间, 通过额外的内存填充来提高内存读取的效率的手段。

总的来说,内存对齐主要解决以下两个问题

【1】跨平台问题:如果数据不对齐,那么在64位字长机器存储的数据可能在32位字长的机器可能就无法正常的读取。

【2】性能问题:如果不对齐,那么每个数据要通过多少次总线传输是未知的,如果每次都要处理这些复杂的情况,那么数据的读/写性能将会收到很大的影响。之所以有些CPU支持访问任意地址,是因为处理器在后面多做了很多额外处理。


(4) 拓展阅读

【1】现代处理器原子操作的实现原理: 1. 处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的. 2. 复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。 3. 实际上大多数原子操作的保证都在硬件层面提供了指令支持,编程语言(C, C++, Go, Java等)无非就是封装一层来保证在不同类型的处理器上能够正确地调用对应的指令。【2】X64系统的地址总线宽度一定是64位吗?32位的系统的地址总线也一定是32位吗? ****真实的情况时大多数X64的系统地址总线只用了48根,也有50根的, X86的系统也是有32根的,也有36根的,这个是由硬件限制的. 因为地址总线宽度代表的是寻址的能力, 正常的48个地址位就已经可以寻址2^48Byte=256TB, 而现在我们的内存在大部分情况下都是没有达到这个阀值的。所以处理器硬件要求传入的地址48~63位地址必须相同,也就是说前48根地址总线足以满足当前很长一段时间内存寻址需求,这个对于32位系统也是同理,2^32byte=4G这也是32位系统最大能搭配4G内存的原因, 再大就找不到了,Pentinum pro/ Pentinum II /Pentinum 系列目前地址总线为36根,支持的最大寻址范围为64G。


二. 内存对齐的规则是什么?

内存对齐主要是为了保证数据的原子读取, 因此内存对齐的最大边界只可能为当前机器的字长。当然如果每种类型都使用最大的对齐边界,那么对内存将是一种浪费,实际上我们只要保证同一个数据不要分开在多次总线事务中便可。

Go在其官方文档 Size and alignment guarantees - golang spec 就描述了其在内存对齐方面的细节。

Go也提供了unsafe.Alignof(x)来返回一个类型的对齐值,并且作出了如下约定:

  • 对于任意类型的变量 x ,unsafe.Alignof(x) 至少为 1。
  • 对于 struct 结构体类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f),unsafe.Alignof(x) 等于其中的最大值。
  • 对于 array 数组类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐倍数。
  • 没有任何字段的空 struct{} 和没有任何元素的 array 占据的内存空间大小为 0,不同的大小为 0 的变量可能指向同一块地址。

总结来说,分为基本类型对齐结构体类型对齐

(1) 基本类型对齐

go语言的基本类型的内存对齐是按照基本类型的大小和机器字长中最小值进行对齐

数据类型

类型大小(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):

代码语言:txt
AI代码解释
复制
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))
}

运行结果:

代码语言:txt
AI代码解释
复制
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

(2) 结构体类型对齐

go语言的结构体的对齐是先对结构体的每个字段进行对齐,然后对总体的大小按照最大对齐边界的整数倍进行对齐。有一个特殊的情况就是,如果空结构体嵌套到一个结构体尾部,那么这个结构体也是要额外对齐的,因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题。

下面通过一些列的例子来说明一下结构体对齐的规则,需要额外说明的是结构体内的字段位置其实都是通过计算到结构体首地址的偏移量来确定的,对所有的字段来说,首地址就是结构体内索引值为0的地址。

案例一

代码语言:txt
AI代码解释
复制
type TestStruct1 struct {
	a int8      //  1 字节====> max align 1 字节
	b int32    //  4 字节====> max align 4 字节
	c []string // 24 字节====> max align 8 字节
}

TestStruct1在编译期就会进行字节对齐的优化。优化后各个变量的相对位置如下图(以64位字长下环境为例):

image.png
image.png

TestStruct1 内存占用大小分析:最大对齐边界为8,总体字节数 = 1 + (align 3) + 4 + 24 = 32, 由于32刚好是8的倍数,所以末尾无需额外填充,最后这个结构体的大小为32字节。

案例二

代码语言:txt
AI代码解释
复制
type TestStruct2 struct {
	a []string     // 24 字节====> max align 8 字节
	b int64       //   8 字节====> max align 8 字节
	c int32       //   4 字节====> max align 4 字节
}
image.png
image.png

TestStruct2 内存占用大小分析:最大对齐边界为8字节,总体字节数 = 24(a) + 8(b) + 4(c) + 4(填充) = 40, 由于40刚好是8的倍数,所以c字段填充完后无需额外填充了。

案例三

代码语言:txt
AI代码解释
复制
type TestStruct3 struct {
	a int8
	b int64
	c struct{}
}
image.png
image.png

TestStruct3 内存占用大小分析:最大对齐边界为8字节,总体字节数 = 1(a)+ 7(填充) + 8(b) + 8(c填充)=24, 空结构体理论上不占字节,但是如果在另一个结构体尾部则需要进行额外字节对齐 。

案例四

代码语言:txt
AI代码解释
复制
type TestStruct4 struct {
    a struct{}
	 b int8
	 c int32
}
image.png
image.png

TestStruct4 内存占用大小分析:最大对齐边界为4字节,总体字节数 = 0(a)+ 1(b)+ 7(填充) + 4(c) = 8。


(3) 测试验证

执行以下代码(环境是64位机器字长的)就可以看到我们之前案例中分析的结果。

代码语言:txt
AI代码解释
复制
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))
}

输出为:

代码语言:txt
AI代码解释
复制
=== 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内存对齐就介绍到这里了,有兴趣的记得点赞哦!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
使用OnBackPressedDispatcher处理回退事件
在单 Activity 多 Fragment 的场景下处理回退按键一直是一件比较恶心的事情。前段时间看 jetpack 在宣传中有提到利用 OnBackPressedDispatcher 处理回退,于是研究了一下。
烧麦程
2022/05/10
1.8K0
使用OnBackPressedDispatcher处理回退事件
Android onBackPressed不执行的解决方案
应用中经常会用到双击返回键在退出应用的效果,目的是为了防止用户误触返回键而导致退出应用。 通常的写法如下: 首先定义一个变量
longzeqiu
2019/08/14
3.5K0
Android 面试题:为什么 Activity 都重建了 ViewModel 还存在?—— Jetpack 系列(3)
ViewModel 是 Jetpack 组件中较常用的组件之一,也是实现 MVVM 模式或 MVI 模式的标准组件之一。在这篇文章里,我将与你讨论 ViewModel 实用和面试常见的知识点。如果能帮上忙请务必点赞加关注,这对我非常重要。
用户9995743
2022/09/26
1.4K0
Android 面试题:为什么 Activity 都重建了 ViewModel 还存在?—— Jetpack 系列(3)
Android项目开发填坑记-Fragment的onBackPressed
版权声明:本文为[他叫自己Mr.张]的原创文章,转载请注明出处,否则禁止转载。 https://micro.blog.csdn.net/article/details/51228934
他叫自己MR.张
2019/07/01
1.2K0
Fragment响应onBackPressed的三个方案
经常会碰到Fragment需要响应activity的onBackPressed事件,对比下三个不同方案
韦东锏
2021/09/29
2.2K0
抽丝剥茧 Jetpack | Lifecycle 到底解决了什么问题?
Lifecycle(生命周期) 在任何 GUI 编程中都是基石般的存在,Android 也不例外。
路遥TM
2022/03/29
3040
抽丝剥茧 Jetpack | Lifecycle 到底解决了什么问题?
“终于懂了“系列:Jetpack AAC完整解析(二)LiveData 完全掌握!
也就是说,LiveData使得 数据的更新 能以观察者模式 被observer感知,且此感知只发生在 LifecycleOwner的活跃生命周期状态。
胡飞洋
2020/12/15
4.1K0
Fragment回退栈及弹出方法
在上一期分享的文章末尾留了一个课后作业,有去思考如何解决吗?如果已经会了那么恭喜你,如果还不会也没关系,本期一起来学习。 一、回退栈 在前面两期的示例中,当我们完成一些操作后,如果想要回到操
分享达人秀
2018/02/05
4.7K1
Fragment回退栈及弹出方法
Lifecycle:生命周期感知型组件的基础 —— Jetpack 系列(1)
Lifecycle 的主要作用是简化实现生命周期感知型组件的复杂度。 在传统的方式中,需要手动从外部宿主(如 Activity、Fragment 或自定义宿主)中将生命周期事件分发到功能组件内部,这势必会造成宿主代码复杂度增加。例如:
用户9995743
2022/09/26
1.2K0
Lifecycle:生命周期感知型组件的基础 —— Jetpack 系列(1)
Jetpack路由组件学习:深入理解功能强大的Navigation架构之接管系统的返回操作
不管你之前用没用过Jetpack Navigation组件,但是或多或少你也可能听说过它。它是Jetpack库中的一个路由组件。此刻你的脑海中可能会浮现阿里ARouter框架。如果你熟悉ARouter但是对Navigation比较陌生,那么你先简单把它们联系在一起,有个直观的感受。
Android技术干货分享
2021/08/05
1.5K0
Jetpack路由组件学习:深入理解功能强大的Navigation架构之接管系统的返回操作
全网最全:Jetpack AAC(-)解析
原文链接:https://juejin.cn/post/6893870636733890574
程序员小顾
2021/12/07
1.4K0
Navigation深入浅出,到出神入化,再到实战改造(二)
NavDestination 节点的封装类对应nav_graph.xml文件中的 </navigation>, </fragment> </activity>, </dialog>目标节点(即Destination),同时有如四个子类:NavGraph,FragmentNavigator#Destination,ActivityNavigator#Destination,DialogFragmentNavigator#Destination
g小志
2022/03/29
2.2K0
Navigation深入浅出,到出神入化,再到实战改造(二)
Fragment 的过去、现在和将来
Fragment 是 Android 中历史十分悠久的一个组件,它在 API 11 被加入,时至今日已成为 Android 开发中最常用的组件之一。Fragment 有了哪些新特性、修复了哪些问题,都是开发者们十分关心的话题。下面我们就来重新说一说 Fragment —— 不仅仅是说现在的 Fragment,还会回顾它的发展,并让您一瞥它未来的样子。
Android 开发者
2020/06/24
9600
Fragment 的过去、现在和将来
Android Jetpack架构组件(四)之LiveData
LiveData是Jetpack架构组件Lifecycle 库的一部分,是一个可感知生命周期的可观察容器类 (Observable)。与常规的可观察类不同,LiveData 具有生命周期感知能力,这意味着它具有感知应用组件(如 Activity、Fragment 或 Service)的生命周期的能力,并且LiveData仅更新处于活跃生命周期状态的应用组件观察者。
xiangzhihong
2020/12/21
3.6K0
Android-Jetpack笔记-Lifecycles
Lifecycle即生命周期,一些业务场景如三方地图、播放器等,依赖于activity的生命周期,会有类似下面的用法:
Holiday
2020/08/10
3830
Android-Jetpack笔记-Lifecycles
Android实现点击两次返回退出APP
这两天在做一个项目碰到这么个问题,需要主界面点击两次直接退出整个APP而不是返回上一个界面,查找了网上的资料,整合和修改了一下写了这篇博客。
全栈程序员站长
2022/09/07
1.6K0
Android APP 快速 Pad 化实现
如何能在最快的时间内,实现一个最新版本 Android app 的 pad 化呢?从拿到一个大型手机 app 代码开始开发到第一个其全新 pad 版本的发布,我们用了不到3个月时间给出了一份满意的答案。 项目背景 采用最新版本手机 APP(之后称为 MyApp)代码,实现其 Pad 化,为平板和大屏手机用户提供更好的体验。为实现 MyApp 的 Pad 化工作,需要我们首先来了解一下 MyApp 项目经典页面的构成以及 Pad 化后的页面结构的变化。 1.MyApp 页面经典构成 现在主流手机 APP 主页
腾讯Bugly
2018/03/23
2.3K0
Android APP 快速 Pad 化实现
Android Jetpack - Lifecycles
Lifecycles 简介 Lifecycles 即生命周期,属于 Jetpack 架构组件之一的 Lifecycles 组件是可以为其它组件提供生命周期感知能力的一个组件,而具备了生命周期感知能力的组件就叫生命周期感知组件,注意加粗部分多读两遍,我看网上很多文章直接把 Lifecycles 组件叫生命周期感知组件那么什么是生命周期感知能力呢?我们知道四大基础组件有生命周期,能感知这些生命周期的组件就具备了生命周期感知能力,所以我认为以前我们使用的 MVP 模式中的 Presenter 如果它通过接口等方式
SkyRiN
2019/08/08
1.4K0
037android初级篇之Activity的几个重要函数
手机屏幕事件的处理方法onTouchEvent。该方法在View类中的定义,并且所有的View子类全部重写了该方法,应用程序可以通过该方法处理手机屏幕的触摸事件。该方法的签名如下所示。
上善若水.夏
2018/09/28
7760
笔记64 | 个人项目“易来”开发记录《二》处理Fragment中返回的问题
需求描述 问题: 一个WebView放在Fragment中,我们都知道webView有一个goBack()方法,可以通过该方法对网页进行后退处理,由于Fragment本身并没有监听onBackPressed的方法,又处于一个Activity中,Activity除又对后退进行了3秒确定退出处理,所有导致 Fragment中的WebView无法进行goBack(); 需求: 需要在Fragment中监听到返回键,当WebView可以后退网页的时候,进行后退网页,当没有可后退的网页时(首页状态),点击返回调Act
项勇
2018/06/19
5950
推荐阅读
相关推荐
使用OnBackPressedDispatcher处理回退事件
更多 >
LV.1
云和恩墨(北京)信息技术有限公司
目录
  • 一. 什么是内存对齐, 为啥要内存对齐?
    • 在解释什么是内存对齐之前,我们需要先了解一下CPU和内存数据交互的过程。CPU和内存是通过总线进行数据交互的。其中地址总线用来传递CPU需要的数据地址,内存将数据通过数据总线传递给CPU, 或者CPU将数据通过数据总线回传给内存。
    • (1) 机器字长
    • (2). 地址总线
    • (3). 数据总线
    • (4) 拓展阅读
  • 二. 内存对齐的规则是什么?
    • (1) 基本类型对齐
    • (2) 结构体类型对齐
    • (3) 测试验证
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档