(自ブログにて改訂版を公開しています)
Go 言語で文字列の連結を行う際にどうやるのが一番速いか,という話。
文字列連結を行う4つの方法
Go 言語で文字列の連結を行う際には概ね以下の4つの方法がある。
string
型は「不変(immutable)」なので,最初の2つが高コストになるだろうことはすぐに想像がつく。
では残りの2つはどうなのかというと
によると最後のが一番速いらしい。ほんじゃまぁ,確かめてみるか。
サンプルコードを用意
以下のコードを使って評価してみる。
package main
import (
"bufio"
"bytes"
"io"
)
//Read content (text data) from buffer
func ContentText(inStream io.Reader) ([]string, error) {
scanner := bufio.NewScanner(inStream)
list := make([]string, 0)
for scanner.Scan() {
list = append(list, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
return list, nil
}
//Write content (text data) to buffer
func WriteBuffer1(lines []string) []byte {
//write to byte buffer
content := make([]byte, 0)
recode := "\r\n"
for _, line := range lines {
content = append(content, line...)
content = append(content, recode...)
}
return content
}
//Write content (text data) to buffer
func WriteBuffer1Cap128(lines []string) []byte {
//write to byte buffer
content := make([]byte, 0, 128) //128 bytes capacity
recode := "\r\n"
for _, line := range lines {
content = append(content, line...)
content = append(content, recode...)
}
return content
}
//Write content (text data) to buffer
func WriteBuffer1Cap1K(lines []string) []byte {
//write to byte buffer
content := make([]byte, 0, 1024) //1K bytes capacity
recode := "\r\n"
for _, line := range lines {
content = append(content, line...)
content = append(content, recode...)
}
return content
}
//Write content (text data) to buffer (buffered I/O)
func WriteBuffer2(lines []string) []byte {
//write to byte buffer
content := bytes.NewBuffer(make([]byte, 0))
recode := "\r\n"
for _, line := range lines {
content.WriteString(line)
content.WriteString(recode)
}
return content.Bytes()
}
//Write content (text data) to buffer (buffered I/O)
func WriteBuffer2Cap128(lines []string) []byte {
//write to byte buffer
content := bytes.NewBuffer(make([]byte, 0, 128)) //128 bytes capacity
recode := "\r\n"
for _, line := range lines {
content.WriteString(line)
content.WriteString(recode)
}
return content.Bytes()
}
//Write content (text data) to buffer (buffered I/O)
func WriteBuffer2Cap1K(lines []string) []byte {
//write to byte buffer
content := bytes.NewBuffer(make([]byte, 0, 1024)) //1K bytes capacity
recode := "\r\n"
for _, line := range lines {
content.WriteString(line)
content.WriteString(recode)
}
return content.Bytes()
}
テストコードはこんな感じ。
package main
import (
"os"
"testing"
)
var list []string
func readFile() {
file, err := os.Open("CollisionsForHashFunctions.txt") //maybe file path
if err != nil {
panic(err)
}
defer file.Close()
list, err = ContentText(file)
if err != nil {
panic(err)
}
}
func BenchmarkWriteBuffer1(b *testing.B) {
readFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
content := WriteBuffer1(list)
_ = content
}
}
func BenchmarkWriteBuffer1Cap128(b *testing.B) {
readFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
content := WriteBuffer1Cap128(list)
_ = content
}
}
func BenchmarkWriteBuffer1Cap1K(b *testing.B) {
readFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
content := WriteBuffer1Cap1K(list)
_ = content
}
}
func BenchmarkWriteBuffer2(b *testing.B) {
readFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
content := WriteBuffer2(list)
_ = content
}
}
func BenchmarkWriteBuffer2Cap128(b *testing.B) {
readFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
content := WriteBuffer2Cap128(list)
_ = content
}
}
func BenchmarkWriteBuffer2Cap1K(b *testing.B) {
readFile()
b.ResetTimer()
for i := 0; i < b.N; i++ {
content := WriteBuffer2Cap1K(list)
_ = content
}
}
入力テキストだが,小さいファイルではテストにならない気がしたので,大昔に書いたテキスト CollisionsForHashFunctions.txt
を使うことにした。サイズは70行,8KB ほど。
テスト結果
結果は以下のとおり。
C:>go test -bench WriteBuffer -benchmen
testing: warning: no tests to run
PASS
BenchmarkWriteBuffer1-4 100000 12220 ns/op 28864 B/op 11 allocs/op
BenchmarkWriteBuffer1Cap128-4 100000 11620 ns/op 28800 B/op 10 allocs/op
BenchmarkWriteBuffer1Cap1K-4 200000 11605 ns/op 27904 B/op 7 allocs/op
BenchmarkWriteBuffer2-4 100000 14200 ns/op 25568 B/op 9 allocs/op
BenchmarkWriteBuffer2Cap128-4 100000 15790 ns/op 26800 B/op 8 allocs/op
BenchmarkWriteBuffer2Cap1K-4 200000 10305 ns/op 17520 B/op 5 allocs/op
ok join 13.260s
ありゃりゃ。 bytes
.Buffer
を使ったほうが速いみたい(capacity を大きくとれば)。
それなら,ファイルサイズを一気に小さくして9行,1KB にしてやってみる。
C:>go test -bench WriteBuffer -benchmen
testing: warning: no tests to run
PASS
BenchmarkWriteBuffer1-4 3000000 443 ns/op 448 B/op 3 allocs/op
BenchmarkWriteBuffer1Cap128-4 5000000 345 ns/op 384 B/op 2 allocs/op
BenchmarkWriteBuffer1Cap1K-4 3000000 561 ns/op 1024 B/op 1 allocs/op
BenchmarkWriteBuffer2-4 1000000 1079 ns/op 544 B/op 4 allocs/op
BenchmarkWriteBuffer2Cap128-4 2000000 727 ns/op 560 B/op 3 allocs/op
BenchmarkWriteBuffer2Cap1K-4 2000000 663 ns/op 1136 B/op 2 allocs/op
ok join 11.987s
今度は []bytes
の方が速くなった。
まぁでも予想通りかな。データのサイズが大きくなればバッファ操作のほうが有利になるのは分かりやすいっちゃあ分かりやすい。注目すべきは BenchmarkWriteBuffer1Cap128
と BenchmarkWriteBuffer1Cap1K
で, capacity を 1KB 取ったほうが若干遅くなっている。この辺のチューニングをどうするか,というところなのだろう。