Skip to content

Instantly share code, notes, and snippets.

@pthethanh
Created April 14, 2023 07:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pthethanh/e7624f54d98633c3e7f0c71415a253b1 to your computer and use it in GitHub Desktop.
Save pthethanh/e7624f54d98633c3e7f0c71415a253b1 to your computer and use it in GitHub Desktop.
Go string concatenation performance comparision
package main
import (
"bytes"
"strings"
"unsafe"
)
func concatStringBuilder(ss ...string) string {
length := len(ss)
if length == 0 {
return ""
}
b := strings.Builder{}
// calculate the buffer size
n := len(ss[0])
for i := 1; i < length; i++ {
n += len(ss[i])
}
// grow the buffer to avoid memory allocation during writing new string.
b.Grow(n)
for i := 0; i < length; i++ {
b.WriteString(ss[i])
}
return b.String()
}
func concatBytesBuffer(ss ...string) string {
length := len(ss)
if length == 0 {
return ""
}
// calculate the buffer size
n := len(ss[0])
for i := 1; i < length; i++ {
n += len(ss[i])
}
b := bytes.Buffer{}
// grow the buffer to avoid memory allocation during writing new string.
b.Grow(n)
for i := 0; i < length; i++ {
b.WriteString(ss[i])
}
return b.String()
}
func concatStringBuilderWithoutGrow(ss ...string) string {
b := strings.Builder{}
for i := 0; i < len(ss); i++ {
b.WriteString(ss[i])
}
return b.String()
}
func concatNaive(ss ...string) string {
rs := ss[0]
for i := 1; i < len(ss); i++ {
rs += ss[i] // cause memory allocation
}
return rs
}
func concatOperator(ss ...string) string {
// TODO assume its len is 10, 100 for benchmark only.
switch len(ss) {
case 10:
return ss[0] + ss[1] + ss[2] + ss[3] + ss[4] + ss[5] + ss[6] + ss[7] + ss[8] + ss[9]
case 100:
return ss[0] + ss[1] + ss[2] + ss[3] + ss[4] + ss[5] + ss[6] + ss[7] + ss[8] + ss[9] +
ss[10] + ss[11] + ss[12] + ss[13] + ss[14] + ss[15] + ss[16] + ss[17] + ss[18] + ss[19] +
ss[20] + ss[21] + ss[22] + ss[23] + ss[24] + ss[25] + ss[26] + ss[27] + ss[28] + ss[29] +
ss[30] + ss[31] + ss[32] + ss[33] + ss[34] + ss[35] + ss[36] + ss[37] + ss[38] + ss[39] +
ss[40] + ss[41] + ss[42] + ss[43] + ss[44] + ss[45] + ss[46] + ss[47] + ss[48] + ss[49] +
ss[50] + ss[51] + ss[52] + ss[53] + ss[54] + ss[55] + ss[56] + ss[57] + ss[58] + ss[59] +
ss[60] + ss[61] + ss[62] + ss[63] + ss[64] + ss[65] + ss[66] + ss[67] + ss[68] + ss[69] +
ss[70] + ss[71] + ss[72] + ss[73] + ss[74] + ss[75] + ss[76] + ss[77] + ss[78] + ss[79] +
ss[80] + ss[81] + ss[82] + ss[83] + ss[84] + ss[85] + ss[86] + ss[87] + ss[88] + ss[89] +
ss[90] + ss[91] + ss[92] + ss[93] + ss[94] + ss[95] + ss[96] + ss[97] + ss[98] + ss[99]
default:
panic("not supported")
}
}
func concatFast(ss ...string) string {
length := len(ss)
if length == 0 {
return ""
}
n := 0
for i := 0; i < length; i++ {
n += len(ss[i])
}
// create & allocate the memory in advance.
b := make([]byte, 0, n)
for i := 0; i < length; i++ {
b = append(b, ss[i]...)
}
// below statement causes a memory allocation.
return string(b)
}
func concatFastImproved(ss ...string) string {
length := len(ss)
if length == 0 {
return ""
}
// create & allocate the memory in advance.
n := 0
for i := 0; i < length; i++ {
n += len(ss[i])
}
b := make([]byte, 0, n)
for i := 0; i < length; i++ {
b = append(b, ss[i]...)
}
return unsafe.String(unsafe.SliceData(b), n)
}
func concatCopy(ss ...string) string {
length := len(ss)
if length == 0 {
return ""
}
// create & allocate the memory in advance.
n := 0
for i := 0; i < length; i++ {
n += len(ss[i])
}
b := make([]byte, n, n)
idx := 0
for i := 0; i < length; i++ {
copy(b[idx:], ss[i])
idx += len(ss[i])
}
return unsafe.String(unsafe.SliceData(b), n)
}
func concatAssignIndex(ss ...string) string {
length := len(ss)
if length == 0 {
return ""
}
// create & allocate the memory in advance.
n := 0
for i := 0; i < length; i++ {
n += len(ss[i])
}
b := make([]byte, n, n)
idx := 0
for i := 0; i < length; i++ {
for j := 0; j < len(ss[i]); j++ {
b[idx] = ss[i][j]
idx++
}
}
return unsafe.String(unsafe.SliceData(b), n)
}
package main
import (
"math/rand"
"strings"
"testing"
"time"
"unsafe"
)
/*
goos: windows
goarch: amd64
pkg: github.com
cpu: 12th Gen Intel(R) Core(TM) i7-1255U
BenchmarkSmall/concat_naive______________________-12 2725071 449.6 ns/op 1520 B/op 9 allocs/op
BenchmarkSmall/concat_string_builder_without_grow-12 3993370 287.7 ns/op 984 B/op 5 allocs/op
BenchmarkSmall/concat_assign_index_______________-12 4681068 255.5 ns/op 288 B/op 1 allocs/op
BenchmarkSmall/concat_bytes_buffer_______________-12 6646387 179.3 ns/op 576 B/op 2 allocs/op
BenchmarkSmall/concat_fast_using_append__________-12 8743449 148.8 ns/op 576 B/op 2 allocs/op
BenchmarkSmall/concat_string_builder_____________-12 12188716 95.24 ns/op 288 B/op 1 allocs/op
BenchmarkSmall/concat_+_operator_________________-12 11912088 96.59 ns/op 288 B/op 1 allocs/op
BenchmarkSmall/concat_fast_improved______________-12 13002703 90.33 ns/op 288 B/op 1 allocs/op
BenchmarkSmall/concat_fast_improved_using_copy___-12 14131878 97.54 ns/op 288 B/op 1 allocs/op
BenchmarkMedium/concat_naive______________________-12 14560 82115 ns/op 509697 B/op 98 allocs/op
BenchmarkMedium/concat_string_builder_without_grow-12 172482 5974 ns/op 34240 B/op 12 allocs/op
BenchmarkMedium/concat_assign_index_______________-12 130524 8269 ns/op 9472 B/op 1 allocs/op
BenchmarkMedium/concat_bytes_buffer_______________-12 447615 2940 ns/op 18944 B/op 2 allocs/op
BenchmarkMedium/concat_fast_using_append__________-12 376473 2819 ns/op 18944 B/op 2 allocs/op
BenchmarkMedium/concat_string_builder_____________-12 655304 1619 ns/op 9472 B/op 1 allocs/op
BenchmarkMedium/concat_+_operator_________________-12 746950 1637 ns/op 9472 B/op 1 allocs/op
BenchmarkMedium/concat_fast_improved______________-12 749499 1613 ns/op 9472 B/op 1 allocs/op
BenchmarkMedium/concat_fast_improved_using_copy___-12 864777 1623 ns/op 9472 B/op 1 allocs/op
BenchmarkBig/concat_naive______________________-12 2 745434800 ns/op 4995658496 B/op 10072 allocs/op
BenchmarkBig/concat_string_builder_without_grow-12 1558 901401 ns/op 5241548 B/op 30 allocs/op
BenchmarkBig/concat_assign_index_______________-12 1155 941113 ns/op 999430 B/op 1 allocs/op
BenchmarkBig/concat_bytes_buffer_______________-12 2985 438088 ns/op 1998854 B/op 2 allocs/op
BenchmarkBig/concat_fast_using_append__________-12 2904 444509 ns/op 1998854 B/op 2 allocs/op
BenchmarkBig/concat_string_builder_____________-12 4792 245516 ns/op 999429 B/op 1 allocs/op
BenchmarkBig/concat_fast_improved______________-12 4142 242516 ns/op 999429 B/op 1 allocs/op
BenchmarkBig/concat_fast_improved_using_copy___-12 4478 260349 ns/op 999430 B/op 1 allocs/op
PASS
ok github.com 39.751s
*/
func BenchmarkSmall(b *testing.B) {
data := []string{"At vero eos et accusamus", "et iusto odio dignissimos", "et iusto odio dignissimos", "praesentium voluptatum", "deleniti atque corrupti quos dolores", "et quas molestias excepturi", "ccaecati cupiditate non provident", "sunt in culpa qui officia deserun", "ollitia animi, id", "um quidem rerum fac"}
benchmark(b, data)
}
func BenchmarkMedium(b *testing.B) {
data := make([]string, 100)
for i := 0; i < 100; i++ {
data[i] = randString(rand.Intn(200))
}
b.ResetTimer()
benchmark(b, data)
}
func BenchmarkBig(b *testing.B) {
data := make([]string, 10_000)
for i := 0; i < 10_000; i++ {
data[i] = randString(rand.Intn(200))
}
b.ResetTimer()
benchmark(b, data)
}
func benchmark(b *testing.B, data []string) {
b.Run("concat naive ", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatNaive(data...)
}
})
b.Run("concat string builder without grow", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatStringBuilderWithoutGrow(data...)
}
})
b.Run("concat assign index ", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatAssignIndex(data...)
}
})
b.Run("concat bytes buffer ", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatBytesBuffer(data...)
}
})
b.Run("concat fast using append ", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatFast(data...)
}
})
b.Run("concat string builder ", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatStringBuilder(data...)
}
})
// TODO just for testing small & medium
if len(data) == 10 || len(data) == 100 {
b.Run("concat + operator ", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatOperator(data...)
}
})
}
b.Run("concat fast improved ", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatFastImproved(data...)
}
})
b.Run("concat fast improved using copy ", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatCopy(data...)
}
})
}
func TestConcat(t *testing.T) {
dataSmall := []string{"At vero eos et accusamus", "et iusto odio dignissimos", "et iusto odio dignissimos", "praesentium voluptatum", "deleniti atque corrupti quos dolores", "et quas molestias excepturi", "ccaecati cupiditate non provident", "sunt in culpa qui officia deserun", "ollitia animi, id", "um quidem rerum fac"}
expect := "At vero eos et accusamuset iusto odio dignissimoset iusto odio dignissimospraesentium voluptatumdeleniti atque corrupti quos doloreset quas molestias excepturiccaecati cupiditate non providentsunt in culpa qui officia deserunollitia animi, idum quidem rerum fac"
v1 := strings.Join(dataSmall, "")
v2 := concatNaive(dataSmall...)
v3 := concatStringBuilder(dataSmall...)
v4 := concatStringBuilderWithoutGrow(dataSmall...)
v5 := concatFast(dataSmall...)
v6 := concatFastImproved(dataSmall...)
v7 := concatCopy(dataSmall...)
v8 := concatAssignIndex(dataSmall...)
v9 := concatOperator(dataSmall...)
ok := expect == v1 && v1 == v2 && v1 == v3 && v1 == v4 && v1 == v5 && v1 == v6 && v1 == v7 && v1 == v8 && v1 == v9
if !ok {
t.Errorf("result is not as expected, want %v", expect)
return
}
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var src = rand.NewSource(time.Now().UnixNano())
// source https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
func randString(n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return *(*string)(unsafe.Pointer(&b))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment