18

Go 的内存对齐和指针运算详解和实践 | Go 技术论坛

 4 years ago
source link: https://learnku.com/articles/39255?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go 的内存对齐和指针运算详解和实践

/ 764 / 0 / 发布于 11个月前 / 更新于 11个月前

uintptr 和 unsafe 普及#

uintptr#

在 Go 的源码中 uintptr 的定义如下:

/* uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
从英文注释可以看出 uintptr是一个整形,它的大小能够容纳任何指针的位模式,它是无符号的,最大值为:18446744073709551615,怎么来的,int64最大值 * 2 +1  
*/
type uintptr uintptr

位模式:内存由字节组成。每个字节由 8 位 bit 组成,每个 bit 状态只能是 0 或 1. 所谓位模式,就是变量所占用内存的所有 bit 的状态的序列
指针大小:一个指针的大小是多少呢?在 32 位操作系统上,指针大小是 4 个字节,在 64 位操作系统上,指针的大小是 8 字节,
所以 uintptr 能够容纳任何指针的位模式,总的说 uintptr 表示的指针地址的值,可以用来进行数值计算
GC 不会把 uintptr 当作指针,uintptr 不会持有一个对象,uintptr 类型的目标会被 GC 回收

unasfe#

在 Go 中,unsafe 是一个包,内容也比较简短,但注释非常多,这个包主要是用来在一些底层编程中,让你能够操作内存地址计算,也就是说 Go 本身是不支持指针运算,但还是留了一个后门,而且 Go 也不建议研发人员直接使用 unsafe 包的方法,因为它绕过了 Go 的内存安全原则,是不安全的,容易使你的程序出现莫名其妙的问题,不利于程序的扩展与维护但为什么说它呢,因为很多框架包括 SDK 中的源代码都用到了这个包的知识,在看源代码时这块不懂,容易懵。下面看看这个包定义了什么?

//ArbitraryType的类型也是int,但它被赋予特殊的含义,代表一个Go的任意表达式类型
type ArbitraryType int

//Pointer是一个int指针类型,在Go种,它是所有指针类型的父类型,也就是说所有的指针类型都可以转化为Pointer, uintptr和Pointer可以相互转化
type Pointer *ArbitraryType

//返回指针变量在内存中占用的字节数(记住,不是变量对应的值占用的字节数)
func Sizeof(x ArbitraryType) uintptr

/*Offsetof返回变量指定属性的偏移量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的属性当作参数*/
func Offsetof(x ArbitraryType) uintptr

//返回变量对齐字节数量
func Alignof(x ArbitraryType) uintptr

什么是内存对齐?为什么要内存对齐?#

在我了解比较深入的语言中(Java Go)都有内存对齐的概念,百度百科对内存对齐的概念是这样定义的:“内存对齐” 应该是编译器的 “管辖范围”。编译器为程序中的每个 “数据单元” 安排在适当的位置上,所谓的数据单元其实就是变量的值。

为什么要内存对齐呢?

  1. 平台原因 (移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常(32 位平台上运行 64 位平台上编译的程序要求必须 8 字节对齐,否则发生 panic)
  2. 性能原因:数据结构 (尤其是栈) 应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

对齐规则:也就是对齐的边界,多少个字节内存对齐,在 32 位操作系统上,是 4 个自己,在 64 位操作系统上是 8 个字节

通过一幅图来理解上面的内容,下图只是举个例子,位数并没有画全

Go

指针运算和内存对齐实践#

内存对齐实践#

理论总是枯燥的,但必须了解,也许看了理论还是不懂,接下来通过实践让你明白

//创建一个变量
var i int8 = 10

//建一个变量转化成Pointer 和 uintptr
p := unsafe.Pointer(&i) //入参必须是指针类型的
fmt.Println(p) //是内存地址0xc0000182da
u := uintptr(i)
fmt.Println(u) //结果就是10

//Pointer转换成uintptr
temp := uintptr(p)
//uintptr转Pointer
p= unsafe.Pointer(u)

//获取指针大小
u = unsafe.Sizeof(p) //传入指针,获取的是指针的大小
fmt.Println(u) // 打印u是:8
 //获取的是变量的大小
u = unsafe.Sizeof(i)
fmt.Println(u) //打印u是:1

//创建两个个结构体
type Person1 struct{
    a bool
    b int64
    c int8
    d string
}
type Person2 struct{
    b int64
    c int8
    a bool
    d string
}
//接下来演示一下内存对齐,猜一猜下面l两个打印值是多少呢?
person1 := Person1{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Sizeof(person1))
person2 := Person2{b:1,c:1,a:true,d:"spw"}
fmt.Println(unsafe.Sizeof(person2))
//第一个结果是40,第二个结果是32,为什么会有这些差距呢?其实就是内存对齐做的鬼,我来详细解释一下

我们知道在 Person1 和 Person2 种变量类型都一样,只是顺序不太一样,
bool 占 1 个字节,
int64 占 8 个字节,
int8 占一个字节,
string 占用 16 个字节,
总的结果应该是 1+8+1+16= 26,为啥 Person1 是 40 呢,Person2 是 32,看下图

Go

根据上图,我们就明白了,在结构体编写中存在内存对齐的概念,而且我们应该小心,尽可能的避免因内存对齐导致结构体大小增大,在书写过程中应该让小字节的变量挨着。我们可以工具进行检测(golangci-lint)。

我们可以通过 func Alignof(x ArbitraryType) uintptr 这个方法返回内存对齐的字节数量,如下代码

type Person1 struct{
    a bool
    b int64
    c int8
    d string
}
p := Person{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Alignof(person))
type Person2 struct{
    a bool
    c int8
}
p1 := Person1{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Alignof(p1))
p2 := Person2{a:true,c:1}
fmt.Println(unsafe.Alignof(p2))
//你任务上面两个println打印多少呢?结果是8,1,在结构体中,内存对齐是按照结构体中最大字节数对齐的(但不会超过8)
指针运算实践#

我们还是用代码来举例说明


type W struct {
   b int32
   c int64
}
var w *W = new(W)
//这时w的变量打印出来都是默认值0,0
fmt.Println(w.b,w.c)

//现在我们通过指针运算给b变量赋值为10
b := unsafe.Pointer(uintptr(unsafe.Pointer(w)) + unsafe.Offsetof(w.b))
*((*int)(b)) = 10
//此时结果就变成了10,0
fmt.Println(w.b,w.c)

解释一下上面的代码
uintptr(unsafe.Pointer(w)) 获取了 w 的指针起始值,
unsafe.Offsetof(w.b) 获取 b 变量的偏移量
两个相加就得到了 b 的地址值,将通用指针 Pointer 转换成具体指针 ((*int)(b)),通过 符号取值,然后赋值,((int)(b)) 相当于把(int) 转换成 int 了,最后对变量重新赋值成 10,这样指针运算就完成了。

关注微信公众号,阅读更多精彩文章

Go
本作品采用《CC 协议》,转载必须注明作者和本文链接
那小子阿伟

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK