안녕하세요!
이번 글에서는 Go 언어에서 문자열을 결합하는 여러 가지 방법을 비교해보려고 합니다.
다양한 방법을 벤치마크 테스트를 통해 성능을 비교하고, 가장 효율적인 방법을 찾아봅시다.
테스트 케이스
아래와 같은 9글자 * 10개의 요소로 이루어진 문자열 배열을 ","로 결합하고, 마지막에 ","를 추가하는 코드를 구현했습니다.
원하는 출력은 string이기 때문에, []byte나 bytes.Buffer를 사용할 경우 마지막에 string으로 캐스팅합니다.
var m = [...]string{
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
}
사용한 코드는 다음과 같습니다:
Golang string join benchmark
package main
import (
"bytes"
"strings"
"testing"
)
var m = [...]string{
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
"AAAAAAAAA",
}
func BenchmarkStringsJoin____(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strings.Join(m[:], ",") + ","
}
}
func BenchmarkAppendOperator_(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 string
for _, v := range m {
m2 += m2 + "," + v
}
m2 += ","
}
}
func BenchmarkHardCoding_____(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = m[0] + "," + m[1] + "," + m[2] + "," + m[3] + "," + m[4] + "," + m[5] + "," + m[6]
}
}
func BenchmarkByteArray______(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 []byte
for _, v := range m {
m2 = append(m2, v...)
m2 = append(m2, ',')
}
_ = string(m2)
}
}
func BenchmarkCapByteArray___(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = make([]byte, 0, 100)
for _, v := range m {
m2 = append(m2, v...)
m2 = append(m2, ',')
}
_ = string(m2)
}
}
func BenchmarkBytesBuffer____(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 bytes.Buffer
for _, v := range m {
m2.Write([]byte(v))
m2.Write([]byte{','})
}
_ = m2.String()
}
}
func BenchmarkCapBytesBuffer_(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = bytes.NewBuffer(make([]byte, 0, 100))
for _, v := range m {
m2.Write([]byte(v))
m2.Write([]byte{','})
}
_ = m2.String()
}
}
func BenchmarkCapBytesBuffer2(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = bytes.NewBuffer(make([]byte, 0, 100))
for _, v := range m {
m2.WriteString(v)
m2.WriteString(",")
}
_ = m2.String()
}
}
구현
+= 연산자 루프
가장 기본적인 구현입니다.
성능이 가장 느립니다.
func BenchmarkAppendOperator(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 string
for _, v := range m {
m2 += m2 + "," + v
}
m2 += ","
}
}
// BenchmarkAppendOperator 1000000 3043 ns/op 3808 B/op 8 allocs/op
결과: 3043ns/op
strings.Join 함수
strings.Join을 사용한 방법입니다.
func BenchmarkStringsJoin(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strings.Join(m[:], ",") + ","
}
}
// BenchmarkStringsJoin 5000000 413 ns/op 240 B/op 3 allocs/op
결과: 413ns/op
한 번의 대입문으로 + 연산자로 모든 요소를 결합
한 번의 대입문으로 모든 요소를 결합하도록 하드코딩합니다.
strings.Join보다 할당이 적고 더 빠릅니다.
func BenchmarkHardCoding(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = m[0] + "," + m[1] + "," + m[2] + "," + m[3] + "," + m[4] + "," + m[5] + "," + m[6]
}
}
// BenchmarkHardCoding 10000000 216 ns/op 80 B/op 1 allocs/op
결과: 216ns/op
[]byte 사용
var m2 []byte로 선언한 []byte에 append()를 반복합니다.
strings.Join보다 느립니다.
func BenchmarkByteArray(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 []byte
for _, v := range m {
m2 = append(m2, v...)
m2 = append(m2, ',')
}
_ = string(m2)
}
}
// BenchmarkByteArray 5000000 613 ns/op 320 B/op 5 allocs/op
결과: 613ns/op
용량을 지정한 []byte
var m2 = make([]byte, 0, 100)로 []byte에 100byte의 용량을 확보한 경우, 할당이 줄어들고 가장 빠른 방법이었습니다.
마지막 string 캐스팅을 제외하면 할당은 0이 됩니다.
func BenchmarkCapByteArray(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = make([]byte, 0, 100)
for _, v := range m {
m2 = append(m2, v...)
m2 = append(m2, ',')
}
_ = string(m2)
}
}
// BenchmarkCapByteArray 10000000 171 ns/op 80 B/op 1 allocs/op
결과: 171ns/op
bytes.Buffer 사용
bytes.Buffer를 사용한 방법입니다.
func BenchmarkBytesBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 bytes.Buffer
for _, v := range m {
m2.Write([]byte(v))
m2.Write([]byte{','})
}
_ = m2.String()
}
}
// BenchmarkBytesBuffer 1000000 1074 ns/op 449 B/op 10 allocs/op
결과: 1074ns/op
용량을 지정한 bytes.Buffer
NewBuffer에 용량을 지정한 []byte를 전달합니다.
func BenchmarkCapBytesBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = bytes.NewBuffer(make([]byte, 0, 100))
for _, v := range m {
m2.Write([]byte(v))
m2.Write([]byte{','})
}
_ = m2.String()
}
}
// BenchmarkCapBytesBuffer 2000000 956 ns/op 419 B/op 10 allocs/op
결과: 956ns/op
용량을 지정한 bytes.Buffer + WriteString
Buffer.Write 대신 Buffer.WriteString 메서드를 사용해봤더니, Buffer.Write보다 빨라졌습니다.
func BenchmarkCapBytesBuffer2(b *testing.B) {
for i := 0; i < b.N; i++ {
var m2 = bytes.NewBuffer(make([]byte, 0, 100))
for _, v := range m {
m2.WriteString(v)
m2.WriteString(",")
}
_ = m2.String()
}
}
// BenchmarkCapBytesBuffer2 5000000 588 ns/op 307 B/op 3 allocs/op
결과: 588ns/op
결과 정리
각 벤치마크 테스트의 결과는 다음과 같습니다.
BenchmarkAppendOperator_ 1000000 3043 ns/op 3808 B/op 8 allocs/op
BenchmarkStringsJoin 5000000 413 ns/op 240 B/op 3 allocs/op
BenchmarkHardCoding 10000000 216 ns/op 80 B/op 1 allocs/op
BenchmarkByteArray 5000000 613 ns/op 320 B/op 5 allocs/op
BenchmarkCapByteArray 10000000 171 ns/op 80 B/op 1 allocs/op
BenchmarkBytesBuffer 1000000 1074 ns/op 449 B/op 10 allocs/op
BenchmarkCapBytesBuffer 2000000 956 ns/op 419 B/op 10 allocs/op
BenchmarkCapBytesBuffer2 5000000 588 ns/op 307 B/op 3 allocs/op
결론
여러 가지 방법의 처리 시간을 대략적으로 비교하면 다음과 같습니다:
[]byte < 하드코딩(200ns) < strings.Join(400ns) < bytes.Buffer(600ns) < += 연산자 루프(2900ns)
- += 연산자 루프는 확실히 한 자리 느립니다.
- 문자열의 + 연산자가 특별히 느리다고 할 수는 없으며,
s := a + b + c
처럼 한 번의 대입으로 여러 결합을 수행하는 것은 충분히 빠릅니다. - bytes.Buffer보다 []byte가 더 빠릅니다.
- make로 충분한 용량을 확보해 할당 횟수를 줄이는 것이 중요합니다.
- bytes.Buffer로 문자열을 결합할 때는 Write()보다 WriteString()이 더 빠릅니다.
Go는 이러한 벤치마크를 쉽게 작성할 수 있어 좋습니다.
성능 최적화가 필요한 경우, 이와 같은 벤치마크를 통해 가장 적합한 방법을 선택할 수 있습니다.
이번 글이 여러분의 Go 프로그래밍에 도움이 되었기를 바랍니다!
'Go' 카테고리의 다른 글
Go 언어는 매력적이다 (0) | 2024.06.01 |
---|---|
Golang에서 로그를 출력하는 팁 (0) | 2024.06.01 |
Go 언어로 문자열 결합 최적화하기: strings.Builder 완벽 가이드 및 벤치마크 (0) | 2024.05.19 |
Go 언어 슬라이스 완벽 이해 - 구현과 활용 (0) | 2024.05.19 |
동적인 요소를 가진 JSON을 깔끔하게 Unmarshal하기 (0) | 2024.05.17 |