由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本身也差不多)。