📝
【Go】効率的な文字列結合の方法
はじめに
大量の文字列結合を効率良く行うにはstrings.Builder
を使うのがおすすめと聞いたことがあるのですが、本当にそうなのか実際に確認したいと思ったので、さまざまな方法を用いて比較してみました!
文字列結合の方法
+演算子
1. main.go
s1, s2 := "Hello", "World!"
result := s1 + ", " + s2
fmt.Println(result) // 出力結果:Hello, World!
特徴
- シンプルなので可読性が高い
- 文字列はイミュータブル(変更不可)なため、新しい文字列を都度作成するためメモリ効率が悪い
fmt.Sprintf()
2. main.go
result = fmt.Sprintf("%s, %s", s1, s2)
fmt.Println(result) // 出力結果:Hello, World!
特徴
- フォーマットが必要な場合に便利
- 文字列はイミュータブル(変更不可)なため、新しい文字列を都度作成するためメモリ効率が悪い
strings.Join()
3. main.go
result = strings.Join([]string{"Hello", "World!"}, ", ")
fmt.Println(result) // 出力結果:Hello, World!
特徴
- メモリ効率が良い
- スライスを作るためのコストはかかる
strings.Builder
4. main.go
var sb strings.Builder
sb.WriteString("Hello")
sb.WriteString(", ")
sb.WriteString("World!")
result = sb.String()
fmt.Println(result) // 出力結果:Hello, World!
特徴
- 文字列のバッファを管理しながら結合できるため、最もメモリ効率が良い
- シンプルな結合をするには冗長になる
ベンチマーク比較
ベンチマークを作成するコマンド
go test -bench=. -benchmem > result.txt
2つの文字列を結合する場合
main_test.go
package main
import (
"fmt"
"strings"
"testing"
)
func BenchmarkStringConcatPlus(b *testing.B) {
s1, s2 := "Hello", "World"
for i := 0; i < b.N; i++ {
_ = s1 + " " + s2
}
}
func BenchmarkStringConcatSprintf(b *testing.B) {
s1, s2 := "Hello", "World"
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s %s", s1, s2)
}
}
func BenchmarkStringConcatJoin(b *testing.B) {
s := []string{"Hello", "World"}
for i := 0; i < b.N; i++ {
_ = strings.Join(s, ", ")
}
}
func BenchmarkStringConcatBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString("Hello")
sb.WriteString(", ")
sb.WriteString("World")
_ = sb.String()
}
}
実行結果
+演算子を用いた場合が最も効率が良く、fmt.Sprintf()が最も効率が悪い
result.txt
goos: darwin
goarch: arm64
pkg: go-strings
cpu: Apple M1
BenchmarkStringConcatPlus-8 73287025 16.08 ns/op 0 B/op 0 allocs/op
BenchmarkStringConcatSprintf-8 15572078 76.39 ns/op 48 B/op 3 allocs/op
BenchmarkStringConcatJoin-8 44943258 26.42 ns/op 16 B/op 1 allocs/op
BenchmarkStringConcatBuilder-8 39316868 30.69 ns/op 24 B/op 2 allocs/op
PASS
ok go-strings 5.677s
1万の文字列を結合する場合
main_test.go
package main
import (
"fmt"
"strings"
"testing"
)
// 1万個の文字列を作成する
const N = 10000
func generateStrings() []string {
s := make([]string, N)
for i := 0; i < N; i++ {
s[i] = "Hello"
}
return s
}
func BenchmarkStringConcatPlus(b *testing.B) {
s := generateStrings()
for i := 0; i < b.N; i++ {
result := ""
for _, v := range s {
result += v + " "
}
_ = result
}
}
func BenchmarkStringConcatSprintf(b *testing.B) {
s := generateStrings()
for i := 0; i < b.N; i++ {
result := ""
for _, v := range s {
result = fmt.Sprintf("%s %s", result, v)
}
_ = result
}
}
func BenchmarkStringConcatJoin(b *testing.B) {
s := generateStrings()
for i := 0; i < b.N; i++ {
_ = strings.Join(s, " ")
}
}
func BenchmarkStringConcatBuilder(b *testing.B) {
s := generateStrings()
for i := 0; i < b.N; i++ {
var sb strings.Builder
for _, v := range s {
sb.WriteString(v)
sb.WriteString(" ")
}
_ = sb.String()
}
}
実行結果
strings.Join()やstrings.Builderが最も効率が良く、fmt.Sprintf()が最も効率が悪い
result.txt
goos: darwin
goarch: arm64
pkg: go-strings
cpu: Apple M1
BenchmarkStringConcatPlus-8 45 25633419 ns/op 324621826 B/op 10015 allocs/op
BenchmarkStringConcatSprintf-8 39 30674366 ns/op 326258795 B/op 30289 allocs/op
BenchmarkStringConcatJoin-8 16693 71537 ns/op 65545 B/op 1 allocs/op
BenchmarkStringConcatBuilder-8 18127 66611 ns/op 285442 B/op 22 allocs/op
PASS
ok go-strings 7.253s
10万の文字列を結合する場合
main_test.go
package main
import (
"fmt"
"strings"
"testing"
)
// 10万個の文字列を作成する
const N = 100000
func generateStrings() []string {
s := make([]string, N)
for i := 0; i < N; i++ {
s[i] = "Hello"
}
return s
}
func BenchmarkStringConcatPlus(b *testing.B) {
s := generateStrings()
for i := 0; i < b.N; i++ {
result := ""
for _, v := range s {
result += v + " "
}
_ = result
}
}
func BenchmarkStringConcatSprintf(b *testing.B) {
s := generateStrings()
for i := 0; i < b.N; i++ {
result := ""
for _, v := range s {
result = fmt.Sprintf("%s %s", result, v)
}
_ = result
}
}
func BenchmarkStringConcatJoin(b *testing.B) {
s := generateStrings()
for i := 0; i < b.N; i++ {
_ = strings.Join(s, " ")
}
}
func BenchmarkStringConcatBuilder(b *testing.B) {
s := generateStrings()
for i := 0; i < b.N; i++ {
var sb strings.Builder
for _, v := range s {
sb.WriteString(v)
sb.WriteString(" ")
}
_ = sb.String()
}
}
実行結果
strings.Join()やstrings.Builderが効率良く、+演算子やfmt.Sprintf()が効率悪い
result.txt
goos: darwin
goarch: arm64
pkg: go-strings
cpu: Apple M1
BenchmarkStringConcatPlus-8 1 4837808791 ns/op 30394873776 B/op 100121 allocs/op
BenchmarkStringConcatSprintf-8 1 4919153375 ns/op 60438743120 B/op 397674 allocs/op
BenchmarkStringConcatJoin-8 1600 748239 ns/op 607211 B/op 1 allocs/op
BenchmarkStringConcatBuilder-8 1360 858256 ns/op 3243932 B/op 31 allocs/op
PASS
ok go-strings 13.592s
それぞれの方法について
+演算子
- 毎回新しい文字列を作成するため、メモリ使用量が膨大になる
- 不要になった中間の文字列が大量にGC(ガベージコレクション)の対象となる¥
fmt.Sprintf()
-
+演算子
同様、毎回新しい文字列を作成するため、メモリ使用量が膨大になる - 内部でバッファを利用するものの、新しいメモリ割り当てが頻繁に発生するため、GCコストが高い
strings.Join()
- 1回だけのメモリ確保で済むため、GC(ガベージコレクション)の負担が少ない
- メモリの割り当て回数(allocs/op)が最小限(通常1回)に抑えられる
- 不要な中間オブジェクトを作らないため、メモリ効率が良い
strings.Builder
- 新しい文字列を毎回作らず、バッファに直接書き込むため、メモリの確保回数が少ない
- 必要に応じてバッファを拡張しながらメモリを再利用 するため、GC負担が小さい
- 動的に文字列を追加するのに長けている
まとめ
- シンプルな文字結合→
+演算子
- フォーマットが絡む結合→
fmt.Sprintf()
- 大量の文字列結合→
strings.Join()
かstrings.Builder
Discussion