零拷贝读取文件成go对象

448 阅读2分钟

我们观察到从文件读取到go对象,需要两次拷贝:

  1. 从文件拷贝到内存,成为[]byte
  2. 从[]byte,按照格式进行读取,拷贝到go对象上

怎么样优化这个读取速度呢?

  1. 利用mmap,把文件直接映射到内存,go允许把这片内存已经转化成[]byte来使用
  2. 直接在这个[]byte上“展开”go对象

所谓”展开“就是一个reinterpret cast,对一个指针的类型重新解读。

var bytes = []byte{
16, 0, 0, 0, 0, 0, 0, 0, 
5, 0, 0, 0, 0, 0, 0, 0, 
'h', 'e', 'l', 'l', 'o'}

假设有这样一个[]byte数组。这个是直接用mmap读取出来的。

var ptr = &bytes[0]

这个ptr就是这片内存区域的指针,指向了开头的第一个元素

type stringHeader struct {
	Data uintptr
	Len  int
}
header := (*stringHeader)(unsafe.Pointer(ptr))

这样我们就把这个内存重新解读为了一个stringHeader了。利用stringHeader就可以构造出string来。

header.Data = uintptr(unsafe.Pointer(&bytes[16]))

把stringHeader的指针指向实际的hello数据部分。

str := (*string)(unsafe.Pointer(ptr))
fmt.Println(str) // "hello"

最后再把同一片内存区域解读为string类型,就得到了"hello"字符串了。整个解码过程只做了一次header.Data的更新,没有做任何内存分配。

相比Java来说,go允许我们使用go自己的heap外的内存。甚至允许把go的对象直接在这片内存上构造出来。这使得我们的应用可以和文件系统的缓存共享一片内存,达到内存利用率的最大化。同时相比protobuf/thrift来说,gocodec就是把cpu对值的内存表示(little endian的integer等),以及go语言对象的内存表示(stringHeader,sliceHeader)直接拷贝了,减少了编解码的计算成本。

完整的代码,欢迎star:bloomfilter_test.go

设计了一个编解码格式叫 github.com/esdb/gocode…

和protobuf的对比还没有测,和json相比,毫无悬念地不在一个量级上。

gocodec 200000 10893 ns/op 288 B/op 2 allocs/op

json 300 3746169 ns/op 910434 B/op 27 allocs/op