24

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

 4 years ago
source link: https://studygolang.com/articles/25866
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.

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个字节

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

Y3UnUnm.png!web

指针运算和内存对齐实践

内存对齐实践

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

//创建一个变量
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,看下图

vmi63yR.png!web

根据上图,我们就明白了,在结构体编写中存在内存对齐的概念,而且我们应该小心,尽可能的避免因内存对齐导致结构体大小增大,在书写过程中应该让小字节的变量挨着。我们可以工具进行检测(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,这样指针运算就完成了。

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

MrueU3n.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK