一文带你读懂结构体内存分配

一个博客引起的血案

一个比较牛逼的博客,介绍了如何优化字符串到字节数组的过程,避免了数据复制过程对程序性能的影响。git

对此,我深感佩服。由于代码很是简单,简单到我根本看不懂!github

package main
 
import (
    "fmt"
    "strings"
    "unsafe"
)
 
func str2bytes(s string) []byte {
    x := (*[2]uintptr)(unsafe.Pointer(&s))
    h := [3]uintptr{x[0], x[1], x[1]}
    return *(*[]byte)(unsafe.Pointer(&h))
}
 
func bytes2str(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}
 
func main() {
    s := strings.Repeat("abc", 3)
    b := str2bytes(s)
    s2 := bytes2str(b)
    fmt.Println(b, s2)
}
复制代码

之因此这么作的缘由是:从 ptype 输出的结构来看,string 可看作 [2]uintptr,而 []byte 则是 [3]uintptr,这便于咱们编写代码,无需额外定义结构类型。如此,str2bytes 只需构建 [3]uintptr{ptr, len, len},而 bytes2str 更简单,直接转换指针类型,忽略掉 cap 便可。golang

关于string[]byte结构能够看下图,我直接复制过来了,你们能够看看上述表达的根据。segmentfault

打击

做者的判断呢,我是相信的。因而,我跃跃欲试的修改了代码,也想完成相似的不用复制变量、仅仅修改指针类型就能够的改变变量类型的过程。下面的代码主要想作的就是,经过修改指针从而完成变量由结构体Num到结构体ReverseNum的转变,代码内容以下:数组

type Num struct {
    name  int8
    value int8
}

type ReverseNum struct {
    value int8
    name  int8
}
func main() {
    n := Num{100, 10}
    z := (*[2]uintptr)(unsafe.Pointer(&n))
    h := [2]uintptr{z[1], z[0]}
    fmt.Println(*(*ReverseNum)(unsafe.Pointer(&h))) // print result is {0, 0}
}
复制代码

可是,结果并不如我所愿。由于打印的结果并非{10, 100},而是{0, 0}。个人自信心受到了挫折,这种转化究竟是什么意思呢???安全

在反复思考没有结果以后,我在著名的stackoverflow贴出了个人疑问。而后就在我打算休息会儿的时候,就有人评论了,给予我深深的打击。函数

对于个人这种写法,人家列出了七点见解。在我缓了缓挫败的心里以后再看的时候,被人删除了六点,惟一剩下的一点就是由于unsafe不够安全。总的来讲我就是在我对go不够熟悉的时候,不要接触或者使用unsafe包。我就在想,什么知识不都是从不熟悉到熟悉的?我就是不够熟悉因此才会在stackoverflow上提问,也就是不熟悉才想熟悉这个知识点而且尝试熟悉这个知识点的啊!性能

unsafe确实不安全,可是并不妨碍我了解这个包啊。优化

而后我就放弃了,毕竟评论的都是大佬,我这种渣渣也许就真的不适合知道这种知识。ui

起色

机缘巧合之下,我又看到了一篇博客,是介绍内存对齐的。其实以前也是看过内存对齐的文章,只不过仅仅是了解下。这篇文章让我想起了以前的疑问,因此我就带着疑问来反复读的这篇博客。

获得的知识和以前看内存对齐的博客是一致的,只不过此次我有了新的感悟。结构体的内存分配确定是和内存对齐相关的。为了获得内存对齐的展现效果,此次没有使用两个都是int8属性的结构体。而是使用了一个新的结构体Student,有两个属性,一个属性是int8,另一个是int64

import (
	"fmt"
	"unsafe"
)

type Student struct {
	age    int8
	salary int64
}

type StudentReverse struct {
	salary int64
	age    int8
}

func main() {
	s := Student{18, 100000}
	x := (*[2]uintptr)(unsafe.Pointer(&s))
	fmt.Println("age is ", *(*int8)(unsafe.Pointer(&x[0])))// 18
}
复制代码

这样打印的结果就是我想要的了,和我在Student初始化的时候赋值一致。而后须要作的就是如何经过指针修改类型了,既然第一步作到了,那么第二步就简单了,根据大佬的博客照葫芦画瓢就行了。

tmp := [2]uintptr{x[1], x[0]}
	studentReverse := *(*StudentReverse)(unsafe.Pointer(&tmp))
	fmt.Println(studentReverse.salary, studentReverse.age)
复制代码

打印的结果和预期一致,新的studentReverse结构体变量就按照预期进行告终构体的变换。

可是这种作法没什么意义,由于uintptr其实就是一个通用的指针,在函数str2bytes中的用法比较trick,不只仅把结构体string中的数组指针做为指针,还把底层数组的长度也做为了指针。而在把Student转化为StudentReverse的过程当中,只不过是把Student中每一个元素值复制了了一份,没有任何意义。

读者能够尝试下修改变量s的属性,看下studentReverse是否也对应的修改了

还剩下最后一个问题,为何贴在stackoverflow的代码就没有成功的运行。仍是由于内存对齐,这两个int8类型的变量,由于内存对齐,放到了一个64位的内存中去了(要看系统支持的位数,个人电脑是64位的)。为了验证正确性呢,能够看以下代码

import (
	"fmt"
	"unsafe"
)

type Test struct {
	a int8
	b int8
}

func main() {
	test := Test{2, 3}
	z := (*[2]int8)(unsafe.Pointer(&test))
	fmt.Println("z is ", z)//z is &[2 3]
	fmt.Printf("totally as one result is %b\n", *(*int16)(unsafe.Pointer(&test)))//totally as one result is 1100000010
}

复制代码

代码运行的结果z中,就是一个长度为2的数组指针,包含有两个值,一个是2(也就是t.a的值),一个是3(也就是t.b的值)。若是把结构体转化为一个int16的变量并按照二进制进行打印,结果是1100000010,若是看的仔细的话,就知道后八位是2,前两位是3。

总的来讲就是每一个结构体地址后面有一段的内存空间,用户存放此结构体的属性。因此就有了unsafe包能够操做地址,操做(*[2]int8)(unsafe.Pointer(&test))就是把变量test的地址以后的16位转化为了长度为2的元素类型为int8的数组,这样就能够直接经过操做指针的方式来操做内存。

可是呢,这些属性由于内存对齐,并非一个一个紧凑而且连续排列的。而内存对齐在不一样的操做系统或者不一样的硬件上的要求也是各不相同。为了不例如你在这个64位系统能够正常运行的操做,到了32位系统就崩溃了,因此 go 就极其不建议使用unsafe包。

总结

虽然以前对于分配很疑惑,颇有挫败感,可是如今也以为没什么?除了开心,也好像没有其余的。任何知识都是一层窗户纸,窗户纸的两边就是两个世界的人。可是你要是觉得你捅破了这层窗户纸你就很厉害,那你错了,由于两个直接中间隔离的仅仅是一层窗户纸。

相关文章
相关标签/搜索