我们观察到从文件读取到go对象,需要两次拷贝:
- 从文件拷贝到内存,成为[]byte
- 从[]byte,按照格式进行读取,拷贝到go对象上
怎么样优化这个读取速度呢?
- 利用mmap,把文件直接映射到内存,go允许把这片内存已经转化成[]byte来使用
- 直接在这个[]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/gocodec
和protobuf的对比还没有测,和json相比,毫无悬念地不在一个量级上。
gocodec 200000 10893 ns/op 288 B/op 2 allocs/op
json 300 3746169 ns/op 910434 B/op 27 allocs/op