零拷貝讀取檔案成 Go 物件

taowen發表於2017-12-06

我們觀察到從檔案讀取到 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/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

更多原創文章乾貨分享,請關注公眾號
  • 零拷貝讀取檔案成 Go 物件
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章