Golang で言語処理100本ノックの No.3 の問題を解いてベンチマークとってみた

## はじめに

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/) の [03. 円周率 の問題](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec03) を

* Golang で解いて
* テストを書いてベンチマークをとってみた

という投稿になります。

## Golang で解いてみた

```go:03.go
package main

import (
	"fmt"
	"regexp"
	"strings"
	_ "unicode/utf8"
)

var sentence string = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."

func main() {

	// 解答例その1
	fmt.Println(answer1())

	// 解答例その2
	fmt.Println(answer2())

}

func answer1() []int {

	// 単語に分解
	words := strings.Split(sentence, " ")

	var pi []int
	for _, word := range words {
		word = strings.Trim(word, ",")
		word = strings.Trim(word, ".")
		// pi = append(pi, utf8.RuneCountInString(word))
		pi = append(pi, len(word))

	}

	return pi

}

func answer2() []int {

	// 文を正規表現で単語に分解
	words := regexp.MustCompile(`\s|"|,|\.`).Split(sentence, -1)

	var pi []int
	for _, word := range words {
		// 文末がスプリットされた場合、words の配列の最後に空文字が入る
		if word != "" {
			// pi = append(pi, utf8.RuneCountInString(word))
			pi = append(pi, len(word))
		}
	}

	return pi

}
```

answer1() は、最初に自分で考えて書いた解き方で、
answer2() 先輩にコードレビューでご指摘を頂いて書きなおした解き方です。

この 2 つの解き方の違いは、（コードを見たらわかりますが）文を単語に分解する箇所です。

answer1() が、単語をまず半角スペースで split してから、
その後にカンマとピリオドを trim して単語を抽出しているのに対して、
answer2() では、正規表現を用いることによって、answer1() と同じことを実現しています。


## テストを書いてベンチマークをとってみた

ベンチマークを取ってみようと思った理由は、
どっちの書き方の方がいいかな？と考えた時に、
answer2() の方が、trim の処理がない分スマートだと思ったのですが、
なんとなく正規表現といえば処理が重そう・・・というイメージがあったので、
answer1() と、answer2() とで、
処理速度にどれくらい差が出るのかな？と疑問に思ったからです。

```go:03_test.go
package main

import (
	"reflect"
	"testing"
)

var expect = []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9}

func TestAnswer1(t *testing.T) {

	answer1 := answer1()
	if !reflect.DeepEqual(answer1, expect) {
		t.Fatalf("%v==%v", answer1, expect)
	}
}

func TestAnswer2(t *testing.T) {

	answer2 := answer2()
	if !reflect.DeepEqual(answer2, expect) {
		t.Fatalf("%v==%v", answer2, expect)
	}

}

func BenchmarkAnswer1(b *testing.B) {

	for i := 0; i < b.N; i++ {
		answer1()
	}
}

func BenchmarkAnswer2(b *testing.B) {

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		answer2()
	}
}

```

以下、ベンチマークの結果です。
 
```bash:
// 文字数カウントに utf8.RuneCountInString() を使用
# go test -bench . 
[ `go test -bench .` | done: 6.1636163s ]
	PASS
	BenchmarkAnswer1	  100000	     20692 ns/op
	BenchmarkAnswer2	   20000	     75057 ns/op
	ok  	_/C_/Users/mypath/study/100-knock	4.900s
```

何度か計測してみましたが、
answer1() と answer2() の処理時間の差でいうと、だいたい 3 倍差に落ち着くという感じでした。

ただ、差が 3 倍といっても、ナノ秒の世界なので、
ミリ秒に変換すると、約 0.02 ミリ秒と約 0.08ミリ秒・・・となり、誤差の範囲とも言えそうです。

あと、ちなみに、
最初は文字数のカウントに utf8.RuneCountInString() を使っていましたが、
今回の問題は英語なので len() を使ったほうが速くなりそうだということで、
こちらもベンチマークを取得してみました。
 
```bash:
// 文字数カウントに len() を使用
# go test -bench . 
[ `go test -bench .` | done: 6.6896689s ]
	PASS
	BenchmarkAnswer1	  100000	     19341 ns/op
	BenchmarkAnswer2	   20000	     59505 ns/op
	ok  	_/C_/Users/mypath/study/100-knock	5.419s
```

ちょっと速くなった模様。

## 結論

* 処理速度に差が出るのかな？
→ 差はある。比較という意味では約 3 倍差。
でもミリ秒にも満たないナノ秒の世界。
作るシステムにもよるけど、一般的には誤差の範囲といってよさそう。

* どっちの書き方の方がいいかな？
→ 英文の単語抽出は、正規表現を使ったほうが簡潔でよさそう。

## 参考URL

* 正規表現：[syntax - The Go Programming Language](http://golang.org/pkg/regexp/syntax/)
* ベンチマーク：[Go でベンチマーク - Block Rockin’ Codes](http://jxck.hatenablog.com/entry/20131123/1385189088)

以上