概要
タイトルには Scanner とか書いているのですが、別段Scanner限定な訳ではないです。
bufio.Scanner 便利ですよねー。これ使ってファイルI/Oしている方多いと思います。
基本的にGoは処理速度が速いので、あまり気にならないと思いますが
とっても大きなファイル、例えば100万行あるテキストファイルとかを扱う場合は
少しでも速く処理してほしくなります。
そんなときに、ちょっこっとだけ役に立つかもしれないTipsです。
内容としては、どの言語でも同じことになりますが、以下となります。
- 入出力はちゃんとバッファリングしましょう
Scannerを使ったサンプルとかで、以下のような形のものがよくあります。
// バッファリングしない状態でのファイルIOのサンプルです。 // // REFERENCES // - https://stackoverflow.com/questions/64638136/performance-issues-while-reading-a-file-line-by-line-with-bufio-newscanner // - https://yourbasic.org/golang/temporary-file-directory/ // - https://pkg.go.dev/os package main import ( "bufio" "fmt" "log" "os" "time" "github.com/devlights/gomy/output" ) func main() { var ( fpath = "/tmp/try-golang-big.txt" file *os.File err error ) if file, err = os.Open(fpath); err != nil { log.Fatalln(err) } defer file.Close() var ( scanner = bufio.NewScanner(file) writer = os.Stdout start = time.Now() ) for scanner.Scan() { fmt.Fprintln(writer, scanner.Text()) } output.Stderrl("[Elapsed]", time.Since(start)) }
特におかしい部分はありません。ちゃんと動いてくれます。
んで、この処理にバカでかいファイル(1行が1024バイトで、1000000行)を食べさせた場合の実行速度が以下のようになったとします。
$ wc /tmp/try-golang-big.txt 1000000 16964980 1025000000 /tmp/try-golang-big.txt $ ls -lh /tmp/try-golang-big.txt -rw-r--r-- 1 gitpod gitpod 978M Mar 29 07:54 /tmp/try-golang-big.txt $ make run-nobuffering go run nobuffering/main.go > /dev/null [Elapsed] 1.414633007s
約 1.4秒ほど。正直全然問題ないのですが、すこしパフォーマンスチューニングできる部分があります。
bufio.Scannerは、bufioパッケージの中の構造体なので、この子は既にバッファリングされています。
なので、調整する必要は特に無くて、今回の場合は見るべきは出力側です。
出力する側は、os.Stdout をそのまま使っています。大量データの場合、ここがボトルネックになります。
なので、出力側をバッファリングしましょう。 bufio.Writer を使います。
// 出力先を os.Stdout から *bufio.Writer に変更したサンプルです。 // // REFERENCES // - https://stackoverflow.com/questions/64638136/performance-issues-while-reading-a-file-line-by-line-with-bufio-newscanner // - https://yourbasic.org/golang/temporary-file-directory/ // - https://pkg.go.dev/os package main import ( "bufio" "fmt" "log" "os" "time" "github.com/devlights/gomy/output" ) func main() { var ( fpath = "/tmp/try-golang-big.txt" file *os.File err error ) if file, err = os.Open(fpath); err != nil { log.Fatalln(err) } defer file.Close() var ( scanner = bufio.NewScanner(file) writer = bufio.NewWriter(os.Stdout) start = time.Now() ) for scanner.Scan() { fmt.Fprintln(writer, scanner.Text()) } output.Stderrl("[Elapsed]", time.Since(start)) }
最初のソースから、writer の部分を bufio.NewWriter にしただけです。
で、実行してみます。
$ make run-buffering1 go run buffering1/main.go > /dev/null [Elapsed] 933.360868ms
少し速くなりました。もうちょいいけます。最後に
scanner.Text()
としている部分。ここで文字列を取得してから書き込んでいますが
このサンプルでは別段文字列にしてなんらかすることは無いのでバイト列でやり取りした方が速いです。
なので、scanner.Bytes()
を使うようにしましょう。
// buffering1 のサンプルにプラスで // scanner.Text() ではなく scanner.Bytes() を // 利用するように変更したサンプルです。 // // REFERENCES // - https://stackoverflow.com/questions/64638136/performance-issues-while-reading-a-file-line-by-line-with-bufio-newscanner // - https://yourbasic.org/golang/temporary-file-directory/ // - https://pkg.go.dev/os package main import ( "bufio" "log" "os" "time" "github.com/devlights/gomy/output" ) func main() { var ( fpath = "/tmp/try-golang-big.txt" file *os.File err error ) if file, err = os.Open(fpath); err != nil { log.Fatalln(err) } defer file.Close() var ( scanner = bufio.NewScanner(file) writer = bufio.NewWriter(os.Stdout) start = time.Now() ) for scanner.Scan() { writer.Write(scanner.Bytes()) writer.WriteByte('\n') } output.Stderrl("[Elapsed]", time.Since(start)) }
出力ストリームに書き出す部分が少し変わりました。
実行してみましょう。
$ make run-buffering2 go run buffering2/main.go > /dev/null [Elapsed] 601.419104ms
さらに少し速くなりました。
やっぱり、大きなデータを扱う場合はバッファリングは有効ですねー。
いつもバッファリングするべきとは思いません。小さなデータであれば素直に読み書きした方が見やすいですしね。
ケースバイケースで。
メモ代わりに、今回使ったMakefileを。
default: gen run-all gen: tr -dc "A-Za-z 0-9" < /dev/urandom | fold -w1024 | head -n 1000000 > /tmp/try-golang-big.txt wc -l /tmp/try-golang-big.txt run-all: run-nobuffering run-buffering1 run-buffering2 run-nobuffering: go run nobuffering/main.go > /dev/null run-buffering1: go run buffering1/main.go > /dev/null run-buffering2: go run buffering2/main.go > /dev/null
最後に実行結果です。
$ make tr -dc "A-Za-z 0-9" < /dev/urandom | fold -w1024 | head -n 1000000 > /tmp/try-golang-big.txt wc -l /tmp/try-golang-big.txt 1000000 /tmp/try-golang-big.txt go run nobuffering/main.go > /dev/null [Elapsed] 1.414633007s go run buffering1/main.go > /dev/null [Elapsed] 933.360868ms go run buffering2/main.go > /dev/null [Elapsed] 601.419104ms
今回のサンプルですが、以下にアップしてあります。
try-golang/examples/singleapp/fileio_performance_tips at master · devlights/try-golang · GitHub
参考情報
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。