strings.Builder를 사용한 문자열 결합은 단순히 +=를 사용하는 것보다 훨씬 빠르다!
특히, strings.Builder.Grow()를 사용해 미리 용량을 확보해두는 방법이 가장 빠르고 사용하기도 좋다.
strings.Builder 내부 살펴보기
strings.Builder는 Write 메서드를 사용해 문자열 등을 효율적으로 구축하기 위해 사용된다.
strings.Builder 자체는 외부로 공개된 필드가 없는 구조체이다.
type Builder struct {
addr *Builder // 값에 의한 복사를 감지하기 위한 수신기
buf []byte
}
내부에 []byte
가 정의되어 있다.
buf
에 추가하기 위한 builder.WriteString()
이 있다.
내부적으로는 단순히 Builder.buf
에 대해 append
가 동작하고 있다.
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
Builder.buf
에 append
한 내용을 마지막에 string
타입으로 반환하면 결합된 문자열이 완성된다.
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
이제 벤치마크를 포함한 실제 사용 예를 살펴보자.
package Builder_test
import (
"strings"
"testing"
)
// 단순히 +=를 사용한 구현
func joinWithPlus(strs ...string) string {
var ret string
for _, str := range strs {
ret += str
}
return ret
}
// Builder.Builder를 사용한 구현
func joinWithBuilder(strs ...string) string {
var sb strings.Builder
for _, str := range strs {
sb.WriteString(str)
}
return sb.String()
}
// 단순히 +=를 사용한 구현의 벤치마크
func BenchmarkPlus(b *testing.B) {
strs := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = joinWithPlus(strs...)
}
}
// Builder.Builder를 사용한 구현의 벤치마크
func BenchmarkBuilder(b *testing.B) {
strs := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = joinWithBuilder(strs...)
}
}
$ go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: go-playground/benchmark/Builder
BenchmarkPlus-12 5000000 292 ns/op 176 B/op 8 allocs/op
BenchmarkBuilder-12 20000000 114 ns/op 56 B/op 3 allocs/op
첫 번째는 +=를 사용한 문자열 결합, 두 번째는 strings.Builder를 사용한 방법이다.
확실히 Builder.Builder
쪽이 더 빠르다!
하지만 여전히 몇 번의 할당이 발생한다.
용량이 있는 []byte에 append하는 것이 더 빠르다!
strings.Builder.WriteString의 구현을 다시 한 번 살펴보자.
type Builder struct {
addr *Builder // 값에 의한 복사를 감지하기 위한 수신기
buf []byte
}
// ...
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
실제는 []byte
에 append()
하는 형태이므로, 용량이 있는 []byte
쪽이 할당 수를 줄여 비용을 확실히 낮출 수 있다!
바로 strings.Builder.buf
의 용량을 설정하고 싶지만, strings.Builder.buf
에는 직접 접근할 수 없다.
하지만 걱정하지 마라. 용량을 추가하는 Grow
메서드가 제공된다.
func (b *Builder) Grow(n int) {}
인수로 전달된 수만큼 buf
의 용량을 확보한다.
이를 통해 용량이 있는 []byte
를 설정할 수 있다.
바로 벤치마크를 해보자.
// Grow를 사용해 용량을 확보한 문자열 결합
func joinWithBuilderAndGrow(strs ...string) string {
var sb strings.Builder
sb.Grow(30)
for _, str := range strs {
sb.WriteString(str)
}
return sb.String()
}
func BenchmarkBuilderAndGrow(b *testing.B) {
strs := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = joinWithBuilderAndGrow(strs...)
}
}
$ go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: go-playground/benchmark/Builder
BenchmarkPlus-12 5000000 289 ns/op 176 B/op 8 allocs/op
BenchmarkBuilder-12 20000000 119 ns/op 56 B/op 3 allocs/op
BenchmarkBuilderAndGrow-12 20000000 64.7 ns/op 32 B/op 1 allocs/op
첫 번째는 +=를 사용한 결합, 두 번째는 strings.Builder를 사용했지만 Grow 메서드를 사용하지 않은 경우다.
세 번째는 Grow도 사용한 경우다. 결과적으로 할당도 줄어들고, 당연히 성능도 향상되었다!
정말 좋다! strings.Builder!
strings.Builder의 편리한 메서드
한 번 초기화한 strings.Builder는 재사용할 수 있다.
특정 문자열을 생성한 후, 빌더를 리셋해서 새로운 문자열을 생성할 수도 있다.
func joinedAndReverse(strs ...string) (string, string) {
var sb strings.Builder
for _, str := range strs {
sb.WriteString(str)
}
joined := sb.String()
// Reset을 호출해 strings.Builder.buf를 nil로 만든다
sb.Reset()
for i := len(strs) - 1; i >= 0; i-- {
sb.WriteString(strs[i])
}
return joined, sb.String()
}
결론
strings.Builder 정말 편리하다!
'Go' 카테고리의 다른 글
Golang에서 로그를 출력하는 팁 (0) | 2024.06.01 |
---|---|
Go의 문자열 결합 성능 비교 (0) | 2024.05.19 |
Go 언어 슬라이스 완벽 이해 - 구현과 활용 (0) | 2024.05.19 |
동적인 요소를 가진 JSON을 깔끔하게 Unmarshal하기 (0) | 2024.05.17 |
Go에서의 소수점 연산과 오차 처리 (0) | 2024.05.17 |