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

胡大海 · · 875 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一个博客引发的血案

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

对此,我深感佩服。因为代码非常简单,简单到我根本看不懂!

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 即可。

关于string[]byte结构可以看下图,我直接复制过来了,大家可以看看上述表达的根据。

打击

作者的判断呢,我是相信的。于是,我跃跃欲试的修改了代码,也想完成类似的不用复制变量、仅仅修改指针类型就可以的改变变量类型的过程。下面的代码主要想做的就是,通过修改指针从而完成变量由结构体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确实不安全,但是并不妨碍我了解这个包啊。

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

转机

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

得到的知识和之前看内存对齐的博客是一致的,只不过这次我有了新的感悟。结构体的内存分配肯定是和内存对齐相关的。为了得到内存对齐的展示效果,这次没有使用两个都是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,如果看的仔细的话,就知道后八位是3,前两位是2。

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

但是呢,这些属性因为内存对齐,并不是一个一个紧凑排列的。而内存对齐在不同的操作系统或者不同的硬件上的要求也是各不相同。为了避免例如你在这个64位系统可以正常运行的操作,到了32位系统就崩溃了,所以 go 就极其不建议使用unsafe包。

总结

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


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:胡大海

查看原文:一文带你读懂结构体内存分配

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

875 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传