当前位置: 首页 > news >正文

runtimeerror什么原因_什么是内存对齐?Go 是否有必要内存对齐?

有些同学可能不知道,struct 中的字段顺序不同,内存占用也有可能会相差很大。比如:

type T1 struct {

a int8

b int64

c int16

}

type T2 struct {

a int8

c int16

b int64

}

在 64 bit 平台上,T1 占用 24 bytes,T2 占用 16 bytes 大小;而在 32 bit 平台上,T1 占用 16 bytes,T2 占用 12 bytes 大小。可见不同的字段顺序,最终决定 struct 的内存大小,所以有时候合理的字段顺序可以减少内存的开销

这是为什么呢?因为有内存对齐的存在,编译器使用了内存对齐,那么最后的大小结果就会不一样。至于为什么要做对齐,主要考虑下面两个原因:

  • 平台(移植性)

    不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况

  • 性能

    若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作,这显然高效很多,是标准的空间换时间做法

有的小伙伴可能会认为内存读取,就是一个简单的字节数组摆放。但实际上 CPU 并不会以一个一个字节去读取和写入内存,相反 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小,块大小我们称其为内存访问粒度。假设访问粒度为 4,那么 CPU 就会以每 4 个字节大小的访问粒度去读取和写入内存。

在不同平台上的编译器都有自己默认的 “对齐系数”。一般来讲,我们常用的 x86 平台的系数为 4;x8664 平台系数为 8。需要注意的是,除了这个默认的对齐系数外,还有不同数据类型的对齐系数。数据类型的对齐系数在不同平台上可能会不一致。例如,在 x8664 平台上,int64 的对齐系数为 8,而在 x86 平台上其对齐系数就是 4。

还是拿上面的 T1、T2 来说,在 x86_64 平台上,T1 的内存布局为:

57670dac94b7bddbc341e60818744474.png

T2 的内存布局为(int16 的对齐系数为 2):

5ff9009816d2787f359c6a7c6863ba4a.png

仔细看,T1 存在许多 padding,显然它占据了不少空间。那么也就不难理解,为什么调整结构体内成员变量的字段顺序就能达到缩小结构体占用大小的疑问了,是因为巧妙地减少了 padding 的存在。让它们更 “紧凑” 了。

其实内存对齐除了可以降低内存占用之外,还有一种情况是必须要手动对齐的:在 x86 平台上原子操作 64bit 指针。之所以要强制对齐,是因为在 32bit 平台下进行 64bit 原子操作要求必须 8 字节对齐,否则程序会 panic。详情可以参考 atomic 官方文档(这么重要的信息竟然放在页面的最底部!!!?):

Bugs

On x86-32, the 64-bit functions use instructions unavailable before the Pentium MMX. On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core. On ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

比如,下面这段代码:

package main

import (

"sync/atomic"

)

type T3 struct {

b int64

c int32

d int64

}

func main() {

a := T3{}

atomic.AddInt64(&a.d, 1)

}

编译为 64bit 可执行文件,运行没有任何问题;但是当编译为 32bit 可执行文件,运行就会 panic:

$ GOARCH=386 go build aligned.go

$

$ ./aligned

panic: runtime error: invalid memory address or nil pointer dereference

[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x8049f2c]

goroutine 1 [running]:

runtime/internal/atomic.Xadd64(0x941218c, 0x1, 0x0, 0x809a4c0, 0x944e070)

/usr/local/go/src/runtime/internal/atomic/asm_386.s:105 +0xc

main.main()

/root/gofourge/src/lab/archive/aligned.go:18 +0x42

原因就是 T3 在 32bit 平台上是 4 字节对齐,而在 64bit 平台上是 8 字节对齐。在 64bit 平台上其内存布局为:

1bd551a498a4a792e943769daf188f0c.png

可以看到编译器为了让 d 8 字节对齐,在 c 后面 padding 了 4 个字节。而在 32bit 平台上其内存布局为:

2b1d08bfe560ca38904f26ce5fa8871d.png

编译器用的是 4 字节对齐,所以 c 后面 4 个字节并没有 padding,而是直接排列 d 的高低位字节。

为了解决这种情况,我们必须手动 padding T3,让其 “看起来” 像是 8 字节对齐的:

type T3 struct {

b int64

c int32

_ int32

d int64

}

这样 T3 的内存布局就变成了:

0712f84fdf90254a1f71b53ec012368f.png

看起来就像 8 字节对齐了一样,这样就能完美兼容 32bit 平台了。其实很多知名的项目,都是这么处理的,比如 groupcache:

type Group struct {

_ int32 // force Stats to be 8-byte aligned on 32-bit platforms

// Stats are statistics on the group.

Stats Stats

}

说了这么多,但是在我们实际编码的时候,多数情况都不会考虑到最优的内存对齐。那有没有什么办法能自动检测当前的内存布局是最优呢?答案是:有的。

golang-sizeof.tips 这个网站就可以可视化 struct 的内存布局,但是只支持 8 字节对齐,是个缺点。还有一种方法,就是用 golangci-lint 做静态检测,比如在我的一个项目中检测结果是这样的:

$ golangci-lint run --disable-all -E maligned

config/config.go:79:11: struct of size 48 bytes could be of size 40 bytes (maligned)

type SASL struct {

^

提示有一处 struct 可以优化,来看一下这个 struct 的定义:

type SASL struct {

Enable bool

Username string

Password string

Handshake bool

}

通过 golang-sizeof.tips 对比,显然字段按照下面这样排序更为合理:

type SASL struct {

Username string

Password string

Handshake bool

Enable bool

}

参考文献

  • On the memory alignment of Go slice values

  • Memory Layouts

  • cmd/vet: detect non-64-bit-aligned arguments to sync/atomic funcs

  • Padding is hard

  • 在 Go 中恰到好处的内存对齐

  • Go unsafe 包之内存布局

推荐阅读

  • 带你领略Go源码的魅力 — Go内存原理详解


喜欢本文的朋友,欢迎关注“Go语言中文网”:

c819e675946f06516804f4e4bed520a1.png

Go语言中文网启用微信学习交流群,欢迎加微信:274768166

相关文章:

  • dubbo源码_Dubbo源码-注册中心
  • python运行不了、显示警告_Python xlrd:禁止显示警告消息
  • linux安装python3环境_Linux环境安装python3
  • 用python打印出一个人的照片_Python用dilb提取照片上人脸的示例
  • getdata提取曲线数据_基于Hypergraph创建曲线(矢量)的结果响应
  • ffmpeg 为取经而来_清华,那个穿越百年而来的白衣少年
  • python 数组 动态赋值_在python中使用numpy创建动态数组
  • java filter 是否能拦截到form表单的所有数据_java 知识点总结(框架篇)
  • python使用xlrd读取xlsx文件_python操作excel文件一(xlrd读取文件)
  • 如何在桌面上显示一行字_只需一个命令启动Hyper-V虚拟机,高手们是如何做到的
  • python相对路径怎么写_Python代码写的丑怎么办?试试这几款神器!
  • java商品管理txt_Java 异常处理的六个建议
  • python里input是什么意思_Tensorflow:标签中的“input”和“input”是什么意思_图像.py在tensorflow示例中...
  • python3自动化软件发布系统pdf_Python 3自动化软件发布系统 -Django 2实战
  • java字符串压缩后反而变长了_不得不爱的数据结构和算法,BAT面试必考,想进大厂不可错过——百战Java课程更新08.17...
  • 0x05 Python数据分析,Anaconda八斩刀
  • Cookie 在前端中的实践
  • Dubbo 整合 Pinpoint 做分布式服务请求跟踪
  • Intervention/image 图片处理扩展包的安装和使用
  • k8s如何管理Pod
  • Koa2 之文件上传下载
  • Promise面试题2实现异步串行执行
  • SSH 免密登录
  • 基于HAProxy的高性能缓存服务器nuster
  • 前端之React实战:创建跨平台的项目架构
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 微信端页面使用-webkit-box和绝对定位时,元素上移的问题
  • 微信小程序:实现悬浮返回和分享按钮
  • AI又要和人类“对打”,Deepmind宣布《星战Ⅱ》即将开始 ...
  • 浅谈sql中的in与not in,exists与not exists的区别
  • 如何在 Intellij IDEA 更高效地将应用部署到容器服务 Kubernetes ...
  • #define、const、typedef的差别
  • #if #elif #endif
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • #我与Java虚拟机的故事#连载14:挑战高薪面试必看
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • %check_box% in rails :coditions={:has_many , :through}
  • (13):Silverlight 2 数据与通信之WebRequest
  • (175)FPGA门控时钟技术
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (pojstep1.3.1)1017(构造法模拟)
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通
  • (附源码)springboot教学评价 毕业设计 641310
  • (附源码)ssm智慧社区管理系统 毕业设计 101635
  • (剑指Offer)面试题41:和为s的连续正数序列
  • (四)Linux Shell编程——输入输出重定向
  • (一)Thymeleaf用法——Thymeleaf简介
  • (原创)Stanford Machine Learning (by Andrew NG) --- (week 9) Anomaly DetectionRecommender Systems...
  • (转)EOS中账户、钱包和密钥的关系
  • (转载)从 Java 代码到 Java 堆
  • ***通过什么方式***网吧
  • . ./ bash dash source 这五种执行shell脚本方式 区别
  • .NET 4.0中的泛型协变和反变