Go语言黑魔法

Go语言黑魔法

今天我要教大家一些无用技能,也可以叫它奇技淫巧或者黑魔法。用得好可以提升性能,用得不好就会招来恶魔,嘿嘿。

黑魔法导论

为了让大家在学习了基础黑魔法之后能有所悟,在必要的时候能创造出本文传授之外的属于自己的魔法,这里需要先给大家打好基础。

学习Go语言黑魔法之前,需要先看清Go世界的本质,你才能获得像Neo一样的能力。

在Go语言中,Slice本质是什么呢?是一个reflect.SliceHeader结构体和这个结构体中Data字段所指向的内存。String本质是什么呢?是一个reflect.StringHeader结构体和这个结构体所指向的内存。

在Go语言中,指针的本质是什么呢?是unsafe.Pointer和uintptr。

当你清楚了它们的本质之后,你就可以随意的玩弄它们,嘿嘿嘿。

第一式 - 获得Slice和String的内存数据

让我小试身手,你有一个CGO接口要调用,需要你把一个字符串数据或者字节数组数据从Go这边传递到C那边,比如像这个:mysql/conn.go at master · funny/mysql · GitHub

查了各种教程和文档,它们都告诉你要用C.GoString或C.GoBytes来转换数据。

但是,当你调用这两个函数的时候,发生了什么事情呢?这时候Go复制了一份数据,然后再把新数据的地址传给C,因为Go不想冒任何风险。

你的C程序只是想一次性的用一下这些数据,也不得不做一次数据复制,这对于一个性能癖来说是多麽可怕的一个事实!

这时候我们就需要一个黑魔法,来做到不拷贝数据又能把指针地址传递给C。

// returns &s[0], which is not allowed in go
func stringPointer(s string) unsafe.Pointer {
	p := (*reflect.StringHeader)(unsafe.Pointer(&s))
	return unsafe.Pointer(p.Data)
}

// returns &b[0], which is not allowed in go
func bytePointer(b []byte) unsafe.Pointer {
	p := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	return unsafe.Pointer(p.Data)
}

以上就是黑魔法第一式,我们先去到Go字符串的指针,它本质上是一个*reflect.StringHeader,但是Go告诉我们这是一个*string,我们告诉Go它同时也是一个unsafe.Pointer,Go说好吧它是,于是你得到了unsafe.Pointer,接着你就躲过了Go的监视,偷偷的把unsafe.Pointer转成了*reflect.StringHeader。

有了*reflect.StringHeader,你很快就取到了Data字段指向的内存地址,它就是Go保护着不想给你看到的隐秘所在,你把这个地址偷偷告诉给了C,于是C就愉快的偷看了Go的隐私。

第二式 - 把[]byte转成string

你肯定要笑,要把[]byte转成string还不简单?Go语言初学者都会的类型转换语法:string(b)。

但是你知道这么做的代价吗?既然我们能随意的玩弄SliceHeader和StringHeader,为什么我们不能造个string给Go呢?Go的内部会不会就是这么做的呢?

先上个实验吧:

package labs28

import "testing"
import "unsafe"

func Test_ByteString(t *testing.T) {
	var x = []byte("Hello World!")
	var y = *(*string)(unsafe.Pointer(&x))
	var z = string(x)

	if y != z {
		t.Fail()
	}
}

func Benchmark_Normal(b *testing.B) {
	var x = []byte("Hello World!")
	for i := 0; i < b.N; i ++ {
		_ = string(x)
	}
}

func Benchmark_ByteString(b *testing.B) {
	var x = []byte("Hello World!")
	for i := 0; i < b.N; i ++ {
		_ = *(*string)(unsafe.Pointer(&x))
	}
}

这个实验先证明了我们可以用[]byte的数据造个string给Go。接着做了两组Benchmark,分别测试了普通的类型转换和伪造string的效率。

结果如下:

$ go test -bench="."
PASS
Benchmark_Normal    20000000            63.4 ns/op
Benchmark_ByteString    2000000000           0.55 ns/op
ok      github.com/idada/go-labs/labs28 2.486s

哟西,显然Go这次又为了稳定性做了些复制数据之类的事情了!这让性能癖怎么能忍受!

我现在手头有个[]byte,但是我想用strconv.Atoi()把它转成字面含义对应的整数值,竟然需要发生一次数据拷贝把它转成string,比如像这样:mysql/types.go at master · funny/mysql · GitHub,这实在不能忍啊!

出招:

// convert b to string without copy
func byteString(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}

我们取到[]byte的指针,这次Go又告诉你它是*byte不是*string,你告诉它滚犊子这是unsafe.Pointer,Go这下又老实了,接着你很自在的把*byte转成了*string,因为你知道reflect.StringHeader和reflect.SliceHeader的结构体只相差末尾一个字段,两者的内存是对齐的,没必要再取Data字段了,直接转吧。

于是,世界终于安宁了,嘿嘿。

第三式 - 结构体和[]byte互转

有一天,你想把一个简单的结构体转成二进制数据保存起来,这时候你想到了encoding/gob和encoding/json,做了一下性能测试,你想到效率有没有可能更高点?

于是你又试了encoding/binady,性能也还可以,但是你还不满意。但是瓶颈在哪里呢?你恍然大悟,最高效的办法就是完全不解析数据也不产生数据啊!

怎么做?是时候使用这个黑魔法了:

type MyStruct struct {
	A int
	B int
}

var sizeOfMyStruct = int(unsafe.Sizeof(MyStruct{}))

func MyStructToBytes(s *MyStruct) []byte {
	var x reflect.SliceHeader
	x.Len = sizeOfMyStruct
	x.Cap = sizeOfMyStruct
	x.Data = uintptr(unsafe.Pointer(s))
	return *(*[]byte)(unsafe.Pointer(&x))
}

func BytesToMyStruct(b []byte) *MyStruct {
	return (*MyStruct)(unsafe.Pointer(
		(*reflect.SliceHeader)(unsafe.Pointer(&b)).Data,
	))
}

这是个曲折但又熟悉的故事。你造了一个SliceHeader,想把它的Data字段指向你的结构体,但是Go又告诉你不可以,你像往常那样把Go提到一边,你得到了unsafe.Pointer,但是这次Go有不死心,它告诉你Data是uintptr,unsafe.Pointer不是uintptr,你大脚把它踢开,怒吼道:unsafe.Pointer就是uintptr,你少拿这些概念糊弄我,Go屁颠屁颠的跑开了,现在你一马平川的来到了函数的出口,Go竟然已经在哪里等着你了!你上前三下五除二把它踢得远远的,顺利的把手头的SliceHeader转成了[]byte。

过了一阵子,你拿到了一个[]byte,你知道需要把它转成MyStruct来读取其中的数据。Go这时候已经完全不是你的对手了,它已经洗好屁股在函数入口等你,你一行代码就解决了它。

第四式 - 用CGO优化GC

你已经是Go世界的Neo,Go跟本没办法拿你怎么样。但是有一天Go的GC突然抽风了,原来这货是不管对象怎么用的,每次GC都给来一遍人口普查,导致系统暂停时间很长。

可是你是个性能癖,你把一堆数据都放在内存里方便快速访问,你这时候很想再踢Go的屁股,但是你没办法,毕竟你还在Go的世界里,你现在得替它擦屁股了,你似乎看到Go躲在一旁偷笑。

你想到你手头有CGO,可以轻易的用C申请到Go世界外的内存,Go的GC不会扫描这部分内存。

你还想到你可以用unsafe.Pointer将C的指针转成Go的结构体指针。于是一大批常驻内存对象被你用这种方式转成了Go世界的黑户,Go的GC一下子轻松了下来。

但是你手头还有很多Slice,于是你就利用C申请内存给SliceHeader来构造自己的Slice,于是你旗下的Slice纷纷转成了Go世界的黑户,Go的GC终于平静了。

但好景总是不长久,有一天Go世界突然崩溃了,只留下一句话:Segmentation Fault。你一下怂了,怎么段错误了?

经过一个通宵排查,你发现你管辖的黑户对象竟然偷偷的跟Go世界的其它合法居民搞在一起,当Go世界以为某个居民已经消亡时,用GC回收了它的住所,但是你的地下世界却认为它还活着,还继续访问它。

于是你废了一番功夫斩断了所有关联,世界暂时宁静了下来。

但是你已经很累了,这时候你想起一句话:

为无为,则无不治
编辑于 2016-06-16

文章被以下专栏收录