Go言語でmmapシステムコールを使ったファイル読み込みの高速化検討とC言語のコンパイラの話
2013.10.14 追記
@kazuho さんからご指摘いただきました!
mmapのほうがreadより速いという迷信について - kazuhoのメモ置き場 -
長いタイトル…。
こないだ書いたgorepっていう検索ツール、もうちょっと速くしたいなと思ってファイル読み込みの部分をmmap()
で置き換える検討中。(ちょっぱやのagもmmap()
を使っている)
mmap()
での高速化確認用にCとGoで簡単なコード書いて実験していたら、以下のことがわかった。
処理速度比較の準備
比較用に書いたのは、open()
/read()
と、open()
/mmap()
、そしてfopen()
/fread()
を行うCとGoのコード。
Goのfread()
は、bufio.Read()
で置き換えている。
- C:
- Go
- open/read https://gist.github.com/ryochack/6021257
- open/mmap https://gist.github.com/ryochack/6020725
- fopen/fread(bufio.Read) https://gist.github.com/ryochack/6021642
Cのコンパイラには以下3つを用意し、最適化オプションは全て-O3
を使用。(実行環境はMac OS X Lion / MacBook Late2008)
- LLVM-GCC4.2 (Xcode付属)
- GCC4.9
- LLVM-Clang3.4
GoはVersion 1.1.1を使用。
ビルドしてできたそれぞれの実行ファイルに、以下の方法で用意した1GBのファイルを読み込ませて、処理時間を測った。
入力データ作成 (1GB) $ dd if=/dev/random of=huge.dat bs=512 count=2097152
実行結果
処理時間の詳細。(10回実行した平均)
open+read | open+mmap | fopen+fread | |
---|---|---|---|
LLVM-GCC4.2 | 2.7149 [s] | 1.8465 [s] | 2.7040 [s] |
GCC4.9 | 2.8684 [s] | 2.0105 [s] | 2.8631 [s] |
LLVM-Clang3.4 | 1.6956 [s] | 0.8303 [s] | 1.6946 [s] |
Go Compiler (ver1.1.1) | 2.4115 [s] | 1.4925 [s] | 2.3830 [s] |
期待どおりに、CでもGoでもopen/read
よりもopen/mmap
の方が速くなってる。
で、コンパイラによる違いについて。
Clangビルドでの処理速度がズバ抜けてる…!
Goの処理速度がLLVM-GCCとGCCよりも速いってのも驚き。
Cのコードがコンパイラによってここまで極端に速度の差が出るとは思っていなかった。
参考
C言語とGo言語で標準出力が端末を参照しているかどうかを判定する
標準出力のディスクリプタを取得して、それが端末を参照しているかどうかを判定する。 使いどころは端末に出力する時と、ファイルにリダイレクト出力する時とで表示の仕方を変えたいとき。
例えば、以下のページの方法でターミナルの文字をカラーにできる。
だけど、これをファイルにリダイレクト出力するとエスケープシーケンスまで記録されてしまい、非常に見づらくなる。
そこで、標準出力がどこに出力されるかを判定し、カラーのON/OFFを切り替える処理を入れるようにしたい。
出力先が端末かどうかの判定は、Cだとこう書く。
#include <stdio.h> #include <unistd.h> int main() { int fd = fileno(stdout); int isTerminal = isatty(fd); printf("fd=%d, isTerm=%d\n", fd, isTerminal); return 0; }
Goだとこう書く。
Cのisatty()
の代わりになる関数が公式パッケージにはなかったので、
http://godoc.org/code.google.com/p/go.crypto/ssh/terminal
のIsTerminal()
を使用。
package main import ( "fmt" "os" term "code.google.com/p/go.crypto/ssh/terminal" ) func main() { fd := os.Stdout.Fd() isTerminal := term.IsTerminal(int(fd)) fmt.Printf("fd=%v, isTerm=%v\n", fd, isTerminal) }
agはどうやって表示の切り替えをやってるんだろうとコード調べたのがきっかけ。 勉強になった。
参考
Go言語でgorepっていう検索ツール書いた
ディレクトリ名やらファイル名やらGrep検索やらを一緒くたに正規表現で検索する"gorep"っていうツール書いた。
https://github.com/ryochack/gorep
以下のコマンドを打ち込むと、カレントディレクトリ以下から再帰的にgo..p
にマッチするディレクトリ、ファイル、テキストファイルの文字列を表示する。
$ gorep -g go..p .
-g
をつけるとgrep検索が有効になる。
Windowsで気軽に検索できるツールが欲しいってのがモチベーションだったけど、まだWindowsで動作検証していないっていう体たらく。
参考
Go言語の並行処理で総和を求める
パタヘネ本の7章(マルチコアとマルチプロセッサとクラスタ)に以下のような(ちょっと違うけど)図が載っていたので、Goの並行処理を使って100,000個の数の和を求めてみた。
最初の図とはちょっと違って、実際の制御はこんな感じ。
値が2つ揃ったら、加算を行うgoroutineを生成する。 goroutineからは、channelを通して合計が送信される。 全ての数を足し切るまでこれを繰り返す。(足す順番は気にしない)
足す数は0〜99,999までの等差数列。
time.Sleep(1 * time.Millisecond)
は、後々の単純総和コードとの比較のために、擬似的に処理を重くしているつもり。(足し算だけだと処理が軽すぎて並行処理にした方が遅かった…)
下は単純に総和を求めるコード。
実行結果
$ time go run sum_goroutine.go 704982704 go run sum_goroutine.go 0.52s user 1.47s system 119% cpu 1.668 total $ time go run sum_simple.go 704982704 go run sum_simple.go 1.23s user 1.75s system 2% cpu 2:07.93 total
単純な総和が2分強かかっているのに対して、goroutineを使うと1.668秒。
本当は、子ルーチン(生成したgoroutine)同士で通信を行なって、親ルーチン(大本のgoroutine)には結果だけを返すように実装したかったけど、channel制御が複雑になってしまって断念。
子ルーチンの計算結果を親ルーチンに返して、それを元にまた子ルーチンを生成する方法で実装した。
channel制御むずかしい…。すぐにdeadlock起こす。
チャネルの配列(スライス)を生成する
Go言語でチャネルの配列(スライス)を生成するには、以下のように宣言する。
チャネルの固定長配列
var chans = []chan int { make(chan int), make(chan int), make(chan int), make(chan int), }
もしくは
var chans [4]chan int for i := range chans { chans[i] = make(chan int) }
チャネルの動的配列
変数numの大きさのチャネル配列確保。
num := 10 ch := make([]chan int, num) for i, _ := range ch { ch[i] = make(chan int) }
2013.4.21 追記
これは配列で、
var chans [4]chan int
こっちはスライスですね。
var chans = []chan int ch := make([]chan int, num)
参考