由GobiesVM的IO.readlines實作論Go讀檔及MRI效能
昨晚寫完 GobiesVM 裡的IO.readlines
函式後,很開心地用了有著100k個單詞的單行文字檔作測試。咦?怎麼一個字都沒有?
第一版的IO.readlines
實作,就如同你在stackoverflow上會找到的一樣,是用bufio.Scanner
來讀文字檔
input, err := os.Open(filename)
scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
}
上面這段code看似沒有問題,在一般的文字檔(例如程式碼本身)測試也都正常,為什麼在遇到大檔的時候就出包了呢?
當我們去看 bufio.Scanner的實作 時,會發現 Scanner
在運作時,會將文字分為一個個的token,並交由SplitFunc
決定如何區分token(預設是斷行),由於是buffered IO,所以會有每個token所能用的buffer的最大size(MaxScanTokenSize = 64 * 1024
),以上面的檔案為例,因為100k個單字全部都集中於一行內,所以很輕易地就超過了這個限制。因此當我將每個詞分至多行後就可以正常運行。
有鑑於這樣的解決之道只是治標不治本,所以讓我們來看看第二個版本吧:
input, _ := os.Open(filename)
defer input.Close()
reader := bufio.NewReader(input)
line, err := reader.ReadString('\n')
for ; err == nil; line, err = reader.ReadString('\n') {
// Do something
}
同樣在bufio
之下有實作io.Reader
的buffered Reader
物件可使用,裡頭也提供了bufio.Reader.ReadLine()
可用,不過在文件內提到一般建議使用bufio.Reader.ReadString('\n')
,而 bufio的實作 內也是後者單純些。
註:這裡雖然有印出內容,但以下的測試皆改為僅只有印出回傳陣列的大小,所以速度會有所不同。
這次測試出來的結果,相當地令人滿意,至少就如同上圖,GobiesVM的IO.readlines
所用的時間都比MRI來得少。
然而事情沒有憨人想得那麼簡單,在不安穩的一覺醒來,我再度用10M行的文字檔作測試時,GobiesVM就原形畢露了。此時的MRI平均只要4秒多就能夠讀取完畢,而GobiesVM則要11秒左右。
當然最便利的藉口就是:GobiesVM沒有作最佳化!所以我就跑去編了個把最佳化(這些選項位於vm_opts.h
裡,如果你想知道的話)全部關掉的MRI出來了…… XD
把最佳化全部拿掉之後的效果十分顯著,完成時間都在30秒以上。
……好吧,這樣的方法基本上只是自嗨,還是得找出一個比較實在點的方式才行。
可能的overhead在哪裡呢?一來當然就是即使Go效能出眾,但在多數時候拖著一個Runtime的它還是比輕盈的C還要來得慢些,二來buffered IO本來就是一個chunk一個chunk地讀進來時作處理,甚至會丟掉在所設定的delimiter後的內容,這樣難免會有些overhead。
所以綜合以上兩點,最偷吃步的方法就是:一口氣將檔案讀進來後再處理斷行:
content, _ := ioutil.ReadFile(filename)
str := string(content[:])
lines := strings.SplitAfter(str, "\n")
for _, line := range lines {
// Do something
}
以下是簡易的效能測試
GobiesVM | MRI 2.0.0 |
---|---|
7.930 | 4.495 |
8.004 | 4.058 |
7.102 | 4.649 |
7.075 | 5.129 |
7.521 | 4.192 |
7.696 | 3.851 |
7.202 | 4.490 |
7.327 | 3.732 |
7.606 | 4.082 |
7.536 | 4.041 |
Mean | |
7.499 | 4.272 |
雖然還是有2~3秒的差距,但其實這也差不多是多數benchmark中Go與C本身的速度差異,所以個人是覺得這樣的結果尚可接受,況且再繼續追下去所花的時間與得到的效益相比可能沒有那麼高。
喔,有些人可能會好奇將檔案全部讀入是否會讓記憶體的使用量飆高,根據不負責的觀察,在使用第二個方法和第三個方法的memory peak並沒有明顯的差異(吃記憶體同樣都吃很兇的意味,而MRI本身也差不多)。