Go言語でmmapシステムコールを使ったファイル読み込みの高速化検討とC言語のコンパイラの話

2013.10.14 追記

@kazuho さんからご指摘いただきました!
mmapのほうがreadより速いという迷信について - kazuhoのメモ置き場 -


長いタイトル…。

こないだ書いたgorepっていう検索ツール、もうちょっと速くしたいなと思ってファイル読み込みの部分をmmap()で置き換える検討中。(ちょっぱやのagmmap()を使っている)

mmap()での高速化確認用にCとGoで簡単なコード書いて実験していたら、以下のことがわかった。

  • ファイル読み込みをmmap()に置き換えると高速化が望めそう
  • Goのコンパイラの最適化はなかなか優秀で、CのGCCビルドよりも速くなることがありそう
  • LLVM-Clangは半端じゃない

処理速度比較の準備

比較用に書いたのは、open()/read()と、open()/mmap()、そしてfopen()/fread()を行うCとGoのコード。 Goのfread()は、bufio.Read()で置き換えている。


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

実行結果

f:id:ryochack:20130718200029p:plain


処理時間の詳細。(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-GCCGCCよりも速いってのも驚き。

Cのコードがコンパイラによってここまで極端に速度の差が出るとは思っていなかった。

参考

C言語とGo言語で標準出力が端末を参照しているかどうかを判定する

標準出力のディスクリプタを取得して、それが端末を参照しているかどうかを判定する。 使いどころは端末に出力する時と、ファイルにリダイレクト出力する時とで表示の仕方を変えたいとき。

例えば、以下のページの方法でターミナルの文字をカラーにできる。

C言語でターミナルで表示される文字をカラー表示させる

だけど、これをファイルにリダイレクト出力するとエスケープシーケンスまで記録されてしまい、非常に見づらくなる。
そこで、標準出力がどこに出力されるかを判定し、カラーの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検索が有効になる。

gorep screenshot

Windowsで気軽に検索できるツールが欲しいってのがモチベーションだったけど、まだWindowsで動作検証していないっていう体たらく。

参考

Go言語の並行処理で総和を求める

パタヘネ本の7章(マルチコアとマルチプロセッサクラスタ)に以下のような(ちょっと違うけど)図が載っていたので、Goの並行処理を使って100,000個の数の和を求めてみた。

f:id:ryochack:20130421173532p:plain

最初の図とはちょっと違って、実際の制御はこんな感じ。

f:id:ryochack:20130424001717p:plain

値が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)

参考