├── Makefile ├── README.md ├── stringconcat.go └── stringconcat_test.go /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | go test -test.bench '.*' ./... 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-string-concat-benchmarks 2 | =========================== 3 | 4 | Benchmarks to compare the different string concatenation methods in Go. For all the details, see [stringconcat_test.go](). 5 | 6 | There is also a [blog post detailing the methodology and results](http://herman.asia/efficient-string-concatenation-in-go). 7 | 8 | In summary, for very few concatenations of short strings (fewer than hundred, length shorter than 10) using naive string appending is just fine. For more heavy-duty cases where efficiency is important, bytes.Buffer is the best choice out of the methods evaluated. strings.Join is a good choice when you already have a string slice that just needs to be concatenated into one string. 9 | 10 | Here are how the methods tested stack up: 11 | 12 | ![Comparison of string concatenation methods in Go](http://img.svbtle.com/rlmmrxjtthkg.png) 13 | 14 | And here are the raw results (also including a benchmark for byte slices): 15 | 16 | ``` 17 | BenchmarkNaiveConcat10-3 2000000 1192 ns/op 360 B/op 11 allocs/op 18 | BenchmarkNaiveConcat100-3 50000 34117 ns/op 26408 B/op 101 allocs/op 19 | BenchmarkNaiveConcat1000-3 500 2641900 ns/op 2694414 B/op 1004 allocs/op 20 | BenchmarkNaiveConcat10000-3 10 188733914 ns/op 271630262 B/op 10339 allocs/op 21 | BenchmarkByteSlice10-3 2000000 717 ns/op 208 B/op 7 allocs/op 22 | BenchmarkByteSlice100-3 300000 4000 ns/op 1552 B/op 10 allocs/op 23 | BenchmarkByteSlice1000-3 30000 53874 ns/op 26128 B/op 16 allocs/op 24 | BenchmarkByteSlice10000-3 2000 701131 ns/op 283668 B/op 24 allocs/op 25 | BenchmarkByteSliceSize10-3 3000000 536 ns/op 200 B/op 4 allocs/op 26 | BenchmarkByteSliceSize100-3 300000 3396 ns/op 1560 B/op 4 allocs/op 27 | BenchmarkByteSliceSize1000-3 30000 40858 ns/op 15896 B/op 4 allocs/op 28 | BenchmarkByteSliceSize10000-3 2000 575724 ns/op 163866 B/op 4 allocs/op 29 | BenchmarkJoin10-3 1000000 1724 ns/op 648 B/op 9 allocs/op 30 | BenchmarkJoin100-3 200000 9183 ns/op 5128 B/op 12 allocs/op 31 | BenchmarkJoin1000-3 20000 82654 ns/op 43528 B/op 15 allocs/op 32 | BenchmarkJoin10000-3 1000 1540246 ns/op 941844 B/op 24 allocs/op 33 | BenchmarkJoinSize10-3 2000000 887 ns/op 312 B/op 5 allocs/op 34 | BenchmarkJoinSize100-3 200000 5981 ns/op 2712 B/op 5 allocs/op 35 | BenchmarkJoinSize1000-3 20000 66210 ns/op 27160 B/op 5 allocs/op 36 | BenchmarkJoinSize10000-3 2000 814003 ns/op 278555 B/op 5 allocs/op 37 | BenchmarkBufferString10-3 1000000 1354 ns/op 400 B/op 8 allocs/op 38 | BenchmarkBufferString100-3 200000 6410 ns/op 2368 B/op 11 allocs/op 39 | BenchmarkBufferString1000-3 20000 56510 ns/op 18880 B/op 14 allocs/op 40 | BenchmarkBufferString10000-3 2000 699816 ns/op 170178 B/op 17 allocs/op 41 | BenchmarkBufferSize10-3 2000000 766 ns/op 312 B/op 5 allocs/op 42 | BenchmarkBufferSize100-3 300000 4548 ns/op 1672 B/op 5 allocs/op 43 | BenchmarkBufferSize1000-3 30000 50437 ns/op 16008 B/op 5 allocs/op 44 | BenchmarkBufferSize10000-3 2000 670182 ns/op 163978 B/op 5 allocs/op 45 | ``` 46 | -------------------------------------------------------------------------------- /stringconcat.go: -------------------------------------------------------------------------------- 1 | package stringconcat 2 | -------------------------------------------------------------------------------- /stringconcat_test.go: -------------------------------------------------------------------------------- 1 | // Package stringconcat exists only to provide benchmarks for the different approaches 2 | // to string concatenation in Go. 3 | package stringconcat 4 | 5 | import ( 6 | "bytes" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | var s []string = []string{} 13 | var junk map[int]string 14 | 15 | func init() { 16 | junk = make(map[int]string) 17 | for i := 0; i < 10000; i++ { 18 | junk[i] = strconv.Itoa(i + 10000) 19 | } 20 | } 21 | 22 | // nextString is an iterator we use to represent a process 23 | // that returns strings that we want to concatenate in order. 24 | func nextString() func() string { 25 | n := 0 26 | // closure captures variable n 27 | return func() string { 28 | n += 1 29 | return junk[n] 30 | } 31 | } 32 | 33 | var global string 34 | 35 | // benchmarkNaiveConcat provides a benchmark for basic built-in 36 | // Go string concatenation. Because strings are immutable in Go, 37 | // it performs the worst of the tested methods. The time taken to 38 | // set up the array that is appended is not counted towards the 39 | // time for naive concatenation. 40 | func benchmarkNaiveConcat(b *testing.B, numConcat int) { 41 | // Reports memory allocations 42 | b.ReportAllocs() 43 | 44 | var ns string 45 | for i := 0; i < b.N; i++ { 46 | next := nextString() 47 | ns = "" 48 | for u := 0; u < numConcat; u++ { 49 | ns += next() 50 | } 51 | } 52 | // we assign to a global variable to make sure compiler 53 | // or runtime optimizations don't skip over the operations 54 | // we were benchmarking. This might be unnecessary, but it's 55 | // safe. 56 | global = ns 57 | } 58 | 59 | func BenchmarkNaiveConcat10(b *testing.B) { 60 | benchmarkNaiveConcat(b, 10) 61 | } 62 | 63 | func BenchmarkNaiveConcat100(b *testing.B) { 64 | benchmarkNaiveConcat(b, 100) 65 | } 66 | 67 | func BenchmarkNaiveConcat1000(b *testing.B) { 68 | benchmarkNaiveConcat(b, 1000) 69 | } 70 | 71 | func BenchmarkNaiveConcat10000(b *testing.B) { 72 | benchmarkNaiveConcat(b, 10000) 73 | } 74 | 75 | // benchmarkByteSlice provides a benchmark for the time it takes 76 | // to repeatedly append returned strings to a byte slice, and 77 | // finally casting the byte slice to string type. 78 | func benchmarkByteSlice(b *testing.B, numConcat int) { 79 | // Reports memory allocations 80 | b.ReportAllocs() 81 | 82 | var ns string 83 | for i := 0; i < b.N; i++ { 84 | next := nextString() 85 | b := []byte{} 86 | for u := 0; u < numConcat; u++ { 87 | b = append(b, next()...) 88 | } 89 | ns = string(b) 90 | } 91 | global = ns 92 | } 93 | 94 | func BenchmarkByteSlice10(b *testing.B) { 95 | benchmarkByteSlice(b, 10) 96 | } 97 | 98 | func BenchmarkByteSlice100(b *testing.B) { 99 | benchmarkByteSlice(b, 100) 100 | } 101 | 102 | func BenchmarkByteSlice1000(b *testing.B) { 103 | benchmarkByteSlice(b, 1000) 104 | } 105 | 106 | func BenchmarkByteSlice10000(b *testing.B) { 107 | benchmarkByteSlice(b, 10000) 108 | } 109 | 110 | // benchmarkByteSlice provides a benchmark for the time it takes 111 | // to repeatedly append returned strings to a byte slice, and 112 | // finally casting the byte slice to string type. 113 | func benchmarkByteSliceSize(b *testing.B, numConcat int) { 114 | // Reports memory allocations 115 | b.ReportAllocs() 116 | 117 | var ns string 118 | for i := 0; i < b.N; i++ { 119 | next := nextString() 120 | b := make([]byte, 0, numConcat*10) 121 | for u := 0; u < numConcat; u++ { 122 | b = append(b, next()...) 123 | } 124 | ns = string(b) 125 | } 126 | global = ns 127 | } 128 | 129 | func BenchmarkByteSliceSize10(b *testing.B) { 130 | benchmarkByteSliceSize(b, 10) 131 | } 132 | 133 | func BenchmarkByteSliceSize100(b *testing.B) { 134 | benchmarkByteSliceSize(b, 100) 135 | } 136 | 137 | func BenchmarkByteSliceSize1000(b *testing.B) { 138 | benchmarkByteSliceSize(b, 1000) 139 | } 140 | 141 | func BenchmarkByteSliceSize10000(b *testing.B) { 142 | benchmarkByteSliceSize(b, 10000) 143 | } 144 | 145 | // benchmarkJoin provides a benchmark for the time it takes to set 146 | // up an array with strings, and calling strings.Join on that array 147 | // to get a fully concatenated string. 148 | func benchmarkJoin(b *testing.B, numConcat int) { 149 | // Reports memory allocations 150 | b.ReportAllocs() 151 | 152 | var ns string 153 | for i := 0; i < b.N; i++ { 154 | next := nextString() 155 | a := []string{} 156 | for u := 0; u < numConcat; u++ { 157 | a = append(a, next()) 158 | } 159 | ns = strings.Join(a, "") 160 | } 161 | global = ns 162 | } 163 | 164 | func BenchmarkJoin10(b *testing.B) { 165 | benchmarkJoin(b, 10) 166 | } 167 | 168 | func BenchmarkJoin100(b *testing.B) { 169 | benchmarkJoin(b, 100) 170 | } 171 | 172 | func BenchmarkJoin1000(b *testing.B) { 173 | benchmarkJoin(b, 1000) 174 | } 175 | 176 | func BenchmarkJoin10000(b *testing.B) { 177 | benchmarkJoin(b, 10000) 178 | } 179 | 180 | // benchmarkJoinSize provides a benchmark for the time it takes to set 181 | // up an array with strings, and calling strings.Join on that array 182 | // to get a fully concatenated string – when the (approximate) number of 183 | // strings is known in advance. 184 | // 185 | // This is identical to benchmarkJoin, except numConcat is used to size 186 | // the []string slice's initial capacity to avoid needless reallocation. 187 | func benchmarkJoinSize(b *testing.B, numConcat int) { 188 | // Reports memory allocations 189 | b.ReportAllocs() 190 | 191 | var ns string 192 | for i := 0; i < b.N; i++ { 193 | next := nextString() 194 | a := make([]string, 0, numConcat) 195 | for u := 0; u < numConcat; u++ { 196 | a = append(a, next()) 197 | } 198 | ns = strings.Join(a, "") 199 | } 200 | global = ns 201 | } 202 | 203 | func BenchmarkJoinSize10(b *testing.B) { 204 | benchmarkJoinSize(b, 10) 205 | } 206 | 207 | func BenchmarkJoinSize100(b *testing.B) { 208 | benchmarkJoinSize(b, 100) 209 | } 210 | 211 | func BenchmarkJoinSize1000(b *testing.B) { 212 | benchmarkJoinSize(b, 1000) 213 | } 214 | 215 | func BenchmarkJoinSize10000(b *testing.B) { 216 | benchmarkJoinSize(b, 10000) 217 | } 218 | 219 | // benchmarkBufferString 220 | func benchmarkBufferString(b *testing.B, numConcat int) { 221 | // Reports memory allocations 222 | b.ReportAllocs() 223 | 224 | var ns string 225 | for i := 0; i < b.N; i++ { 226 | next := nextString() 227 | buffer := bytes.NewBufferString("") 228 | for u := 0; u < numConcat; u++ { 229 | buffer.WriteString(next()) 230 | } 231 | ns = buffer.String() 232 | } 233 | global = ns 234 | } 235 | 236 | func BenchmarkBufferString10(b *testing.B) { 237 | benchmarkBufferString(b, 10) 238 | } 239 | 240 | func BenchmarkBufferString100(b *testing.B) { 241 | benchmarkBufferString(b, 100) 242 | } 243 | 244 | func BenchmarkBufferString1000(b *testing.B) { 245 | benchmarkBufferString(b, 1000) 246 | } 247 | 248 | func BenchmarkBufferString10000(b *testing.B) { 249 | benchmarkBufferString(b, 10000) 250 | } 251 | 252 | func benchmarkBufferSize(b *testing.B, numConcat int) { 253 | // Reports memory allocations 254 | b.ReportAllocs() 255 | 256 | var ns string 257 | for i := 0; i < b.N; i++ { 258 | next := nextString() 259 | buffer := bytes.NewBuffer(make([]byte, 0, numConcat*10)) 260 | for u := 0; u < numConcat; u++ { 261 | buffer.WriteString(next()) 262 | } 263 | ns = buffer.String() 264 | } 265 | global = ns 266 | } 267 | 268 | func BenchmarkBufferSize10(b *testing.B) { 269 | benchmarkBufferSize(b, 10) 270 | } 271 | 272 | func BenchmarkBufferSize100(b *testing.B) { 273 | benchmarkBufferSize(b, 100) 274 | } 275 | 276 | func BenchmarkBufferSize1000(b *testing.B) { 277 | benchmarkBufferSize(b, 1000) 278 | } 279 | 280 | func BenchmarkBufferSize10000(b *testing.B) { 281 | benchmarkBufferSize(b, 10000) 282 | } 283 | --------------------------------------------------------------------------------