From 12dac713000661773c0070300f4370445362d8c2 Mon Sep 17 00:00:00 2001 From: Arthur Kushman Date: Thu, 4 May 2023 11:21:32 +0300 Subject: [PATCH] #67: Implement ConcatFast that concatenates strings faster than strings.Builder (#68) --- str.go | 21 +++++++++++++ str_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/str.go b/str.go index 4ef2535..d107ba9 100644 --- a/str.go +++ b/str.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "strings" + "unsafe" ) type replaceParams struct { @@ -129,3 +130,23 @@ func HTTPBuildQuery(pairs map[string]interface{}) string { return q.Encode() } + +// ConcatFast concatenates strings faster than strings.Builder (see benchmarks) +func ConcatFast(s ...string) string { + l := len(s) + if l == 0 { + return "" + } + + n := 0 + for i := 0; i < l; i++ { + n += len(s[i]) + } + + b := make([]byte, 0, n) + for i := 0; i < l; i++ { + b = append(b, s[i]...) + } + + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/str_test.go b/str_test.go index d07a64f..b121073 100644 --- a/str_test.go +++ b/str_test.go @@ -1,10 +1,15 @@ package pgo_test import ( + "math/rand" + "strings" "testing" + "time" - "github.com/arthurkushman/pgo" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/arthurkushman/pgo" ) func TestStrReplace(t *testing.T) { @@ -74,3 +79,86 @@ func TestHTTPBuildQuery(t *testing.T) { queryStr2 := pgo.HTTPBuildQuery(map[string]interface{}{}) assert.Empty(t, queryStr2, "built str from an empty map must be empty") } + +func TestConcatFast(t *testing.T) { + tests := []struct { + name string + s []string + result string + }{ + { + name: "concat 3 strings", + s: []string{"foo", "bar", "bazzz"}, + result: "foobarbazzz", + }, + { + name: "concat 0 strings", + s: []string{}, + result: "", + }, + { + name: "concat random strings", + s: []string{"impEdfCJyek3jn5kj3nkj35nkj35nkj3nkj3n5kjn3kjn35kjn5", "IpDtUOSwMy", "sMIaQYdeON", "TZTwRNgZfx", + "kybtlfzfJa", "UJQJXhknLe", "GKDmxroeFv", + "ifguLESWvm333334241341231242414k12m4k1m24k1m2k4m1k24n1l2n41ln41lk2n4k12"}, + result: "impEdfCJyek3jn5kj3nkj35nkj35nkj3nkj3n5kjn3kjn35kjn5IpDtUOSwMysMIaQYdeONTZTwRNgZfxkybtlfzfJaUJQJXhknLeGKDmxroeFvifguLESWvm333334241341231242414k12m4k1m24k1m2k4m1k24n1l2n41ln41lk2n4k12", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + resStr := pgo.ConcatFast(tc.s...) + require.Equal(t, tc.result, resStr) + }) + } +} + +func BenchmarkConcatFast(b *testing.B) { + s := generateRandomSliceOfStrings() + for i := 0; i < b.N; i++ { + pgo.ConcatFast(s...) + } +} + +func BenchmarkConcatFast2(b *testing.B) { + s := generateRandomSliceOfStrings() + for i := 0; i < b.N; i++ { + stringBuilder(s...) + } +} + +func generateRandomSliceOfStrings() []string { + const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + rand.Seed(time.Now().UnixNano()) + s := make([]string, 15) + for i := range s { + bt := make([]byte, 10) + for j := range bt { + bt[j] = letterBytes[rand.Intn(len(letterBytes))] + } + s[i] = string(bt) + } + + return s +} + +func stringBuilder(s ...string) string { + l := len(s) + if l == 0 { + return "" + } + + b := strings.Builder{} + n := 0 + for i := 0; i < l; i++ { + n += len(s[i]) + } + + b.Grow(n) + for i := 0; i < l; i++ { + b.WriteString(s[i]) + } + + return b.String() +}