├── LICENSE ├── README.md ├── concat_amd64.go ├── concat_amd64.s ├── concat_generic.go ├── concat_test.go └── example_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Iskander Sharipov / Quasilyte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Concat 2 | 3 | Demo package for [Ускорение конкатенации строк в Go своими руками](https://habr.com/post/417479/) article. 4 | 5 | ### Overview 6 | 7 | This package provides simple functions that return concatenation results. 8 | Can work faster than Go `+` operator. 9 | 10 | You should not use this package, really. It's just an example. 11 | 12 | ### Benchmarks 13 | 14 | ``` 15 | BenchmarkConcat2Operator/short-8 20000000 84.4 ns/op 16 | BenchmarkConcat2Operator/longer-8 10000000 158 ns/op 17 | BenchmarkConcat2Builder/short-8 20000000 70.7 ns/op 18 | BenchmarkConcat2Builder/longer-8 10000000 127 ns/op 19 | BenchmarkConcat2/short-8 30000000 57.3 ns/op 20 | BenchmarkConcat2/longer-8 20000000 106 ns/op 21 | BenchmarkConcat3Operator/short-8 20000000 103 ns/op 22 | BenchmarkConcat3Operator/longer-8 10000000 217 ns/op 23 | BenchmarkConcat3Builder/short-8 20000000 89.9 ns/op 24 | BenchmarkConcat3Builder/longer-8 5000000 249 ns/op 25 | BenchmarkConcat3/short-8 20000000 85.0 ns/op 26 | BenchmarkConcat3/longer-8 10000000 189 ns/op 27 | ``` 28 | 29 | Number one is unsafe concatenation, second is `strings.Builder` with preallocated 30 | buffer and "obvious" concatenation is the slowest one... unless [CL123256](https://go-review.googlesource.com/c/go/+/123256) is applied. 31 | 32 | Using the `benchstat`, here is the difference between `concat` and `+`: 33 | 34 | ``` 35 | name old time/op new time/op delta 36 | Concat2/short-8 84.4ns ± 2% 64.3ns ± 4% -23.85% (p=0.000 n=14+15) 37 | Concat2/longer-8 138ns ± 1% 118ns ± 1% -14.83% (p=0.000 n=13+15) 38 | Concat3/short-8 105ns ± 5% 82ns ± 5% -22.29% (p=0.000 n=15+14) 39 | Concat3/longer-8 218ns ± 1% 192ns ± 1% -11.95% (p=0.000 n=15+15) 40 | ``` 41 | 42 | If compared with AMD64 asm version for concat2: 43 | 44 | ``` 45 | name old time/op new time/op delta 46 | Concat2/short-8 84.4ns ± 0% 56.9ns ± 5% -32.54% (p=0.000 n=15+15) 47 | Concat2/longer-8 138ns ± 1% 107ns ± 0% -22.51% (p=0.000 n=13+15) 48 | ``` 49 | 50 | As a bonus, asm version also makes empty strings concatenation optimization, 51 | just like runtime version of concat would. 52 | 53 | ### Example 54 | 55 | ```go 56 | package main 57 | 58 | import ( 59 | "fmt" 60 | 61 | "github.com/Quasilyte/concat" 62 | ) 63 | 64 | func main() { 65 | v := "world!" 66 | fmt.Println(concat.Strings("hello, ", v)) // => "hello, world!" 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /concat_amd64.go: -------------------------------------------------------------------------------- 1 | package concat 2 | 3 | // Strings returns x+y concatenation result. 4 | func Strings(x, y string) string 5 | -------------------------------------------------------------------------------- /concat_amd64.s: -------------------------------------------------------------------------------- 1 | #include "textflag.h" 2 | #include "funcdata.h" 3 | 4 | TEXT ·Strings(SB), 0, $48-48 5 | NO_LOCAL_POINTERS // Hack. 6 | MOVQ x+0(FP), DX 7 | MOVQ x+8(FP), DI 8 | MOVQ y+16(FP), CX 9 | MOVQ y+24(FP), SI 10 | TESTQ DI, DI 11 | JZ maybe_return_y // x is "", maybe we can return y without allocs 12 | TESTQ SI, SI 13 | JZ maybe_return_x // y is "", maybe we can return x without allocs 14 | concatenate: 15 | LEAQ (DI)(SI*1), R8 // len(x) + len(y) 16 | // Allocate storage for new string. 17 | MOVQ R8, 0(SP) 18 | MOVQ $0, 8(SP) 19 | MOVB $0, 16(SP) 20 | CALL runtime·mallocgc(SB) 21 | MOVQ 24(SP), AX // allocated str 22 | MOVQ AX, newstr-8(SP) 23 | // Copy x into allocated str. 24 | MOVQ x+0(FP), DX 25 | MOVQ x+8(FP), DI 26 | MOVQ AX, 0(SP) 27 | MOVQ DX, 8(SP) 28 | MOVQ DI, 16(SP) 29 | CALL runtime·memmove(SB) 30 | // Copy y into allocated str at the offset of len(x). 31 | MOVQ x+8(FP), DI 32 | MOVQ y+16(FP), CX 33 | MOVQ y+24(FP), SI 34 | MOVQ newstr-8(SP), AX 35 | LEAQ (AX)(DI*1), BX 36 | MOVQ BX, 0(SP) 37 | MOVQ CX, 8(SP) 38 | MOVQ SI, 16(SP) 39 | CALL runtime·memmove(SB) 40 | // Return new string. 41 | MOVQ newstr-8(SP), AX 42 | MOVQ x+8(FP), R8 43 | ADDQ y+24(FP), R8 44 | MOVQ AX, ret+32(FP) 45 | MOVQ R8, ret+40(FP) 46 | RET 47 | maybe_return_y: 48 | MOVQ (TLS), AX // stack 49 | CMPQ CX, (AX) 50 | JL return_y // if y_ptr < stk.lo 51 | CMPQ CX, 8(AX) 52 | JGE return_y // if y_ptr >= stk.hi 53 | JMP concatenate // y is on stack, must do a new alloc 54 | return_y: 55 | MOVQ CX, ret+32(FP) 56 | MOVQ SI, ret+40(FP) 57 | RET 58 | maybe_return_x: 59 | MOVQ (TLS), AX // stack 60 | CMPQ DX, (AX) 61 | JL return_x // if x_ptr < stk.lo 62 | CMPQ DX, 8(AX) 63 | JGE return_x // if x_ptr >= stk.hi 64 | JMP concatenate // x is on stack, must do a new alloc 65 | return_x: 66 | MOVQ DX, ret+32(FP) 67 | MOVQ DI, ret+40(FP) 68 | RET 69 | -------------------------------------------------------------------------------- /concat_generic.go: -------------------------------------------------------------------------------- 1 | // +build !amd64 2 | 3 | package concat 4 | 5 | // Strings returns x+y concatenation result. 6 | // Works faster than Go "+" operator if neither of strings is empty. 7 | func Strings(x, y string) string { 8 | length := len(x) + len(y) 9 | if length == 0 { 10 | return "" 11 | } 12 | b := make([]byte, length) 13 | copy(b, x) 14 | copy(b[len(x):], y) 15 | return goString(&b[0], length) 16 | } 17 | -------------------------------------------------------------------------------- /concat_test.go: -------------------------------------------------------------------------------- 1 | package concat 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "unsafe" 7 | ) 8 | 9 | func TestStringSize(t *testing.T) { 10 | have := unsafe.Sizeof(stringStruct{}) 11 | want := unsafe.Sizeof("") 12 | if have != want { 13 | t.Errorf("string struct size mismatch: have %d, want %d", have, want) 14 | } 15 | } 16 | 17 | func TestConcat(t *testing.T) { 18 | const stringConst = "const" 19 | var stringVar = "var" 20 | 21 | inputs := [][]string{ 22 | // concat2: 23 | {"", ""}, 24 | {"x", ""}, 25 | {"", "x"}, 26 | {stringVar, stringConst}, 27 | {"abc", stringVar}, 28 | {stringConst, "abc"}, 29 | {stringVar + stringVar, stringVar}, 30 | 31 | // concat3: 32 | {"", "", ""}, 33 | {"x", "", ""}, 34 | {"", "x", ""}, 35 | {stringVar, stringConst, "x"}, 36 | {stringConst, "", ""}, 37 | {stringVar, "x", stringVar + stringVar}, 38 | } 39 | 40 | reverseStrings := func(xs []string) []string { 41 | ys := make([]string, len(xs)) 42 | for i := range xs { 43 | ys[i] = xs[len(xs)-i-1] 44 | } 45 | return ys 46 | } 47 | 48 | type testCase struct { 49 | fn func(args []string) string 50 | goldenFn func(args []string) string 51 | args []string 52 | } 53 | var tests []testCase 54 | for _, xs := range inputs { 55 | var fn func([]string) string 56 | var goldenFn func([]string) string 57 | switch len(xs) { 58 | case 2: 59 | fn = func(xs []string) string { 60 | return Strings(xs[0], xs[1]) 61 | } 62 | goldenFn = func(xs []string) string { 63 | return xs[0] + xs[1] 64 | } 65 | case 3: 66 | fn = func(xs []string) string { 67 | return Strings3(xs[0], xs[1], xs[2]) 68 | } 69 | goldenFn = func(xs []string) string { 70 | return xs[0] + xs[1] + xs[2] 71 | } 72 | default: 73 | panic("invalid arguments count") 74 | } 75 | 76 | tests = append(tests, 77 | testCase{fn: fn, goldenFn: goldenFn, args: xs}, 78 | testCase{fn: fn, goldenFn: goldenFn, args: reverseStrings(xs)}) 79 | } 80 | 81 | for _, test := range tests { 82 | have := test.fn(test.args) 83 | want := test.goldenFn(test.args) 84 | if have != want { 85 | t.Errorf("concat(%v) result mismatch:\nhave: %q\nwant: %q", 86 | test.args, have, want) 87 | } 88 | } 89 | } 90 | 91 | var shortStrings = []string{"lorem ", "ipsum ", "dolor sit amet"} 92 | var longerStrings = []string{ 93 | strings.Repeat(shortStrings[0], 16), 94 | strings.Repeat(shortStrings[1], 16), 95 | strings.Repeat(shortStrings[2], 16), 96 | } 97 | 98 | func benchmarkConcat(b *testing.B, fn func([]string) string) { 99 | b.Run("short", func(b *testing.B) { 100 | for i := 0; i < b.N; i++ { 101 | _ = fn(shortStrings) 102 | } 103 | }) 104 | b.Run("longer", func(b *testing.B) { 105 | for i := 0; i < b.N; i++ { 106 | _ = fn(longerStrings) 107 | } 108 | }) 109 | } 110 | 111 | func BenchmarkConcat2Operator(b *testing.B) { 112 | benchmarkConcat(b, func(xs []string) string { return xs[0] + xs[1] }) 113 | } 114 | 115 | func BenchmarkConcat2Builder(b *testing.B) { 116 | benchmarkConcat(b, func(xs []string) string { 117 | var builder strings.Builder 118 | builder.Grow(len(xs[0]) + len(xs[1])) 119 | builder.WriteString(xs[0]) 120 | builder.WriteString(xs[1]) 121 | return builder.String() 122 | }) 123 | } 124 | 125 | func BenchmarkConcat2(b *testing.B) { 126 | benchmarkConcat(b, func(xs []string) string { 127 | return Strings(xs[0], xs[1]) 128 | }) 129 | } 130 | 131 | func BenchmarkConcat3Operator(b *testing.B) { 132 | benchmarkConcat(b, func(xs []string) string { 133 | return xs[0] + xs[1] + xs[2] 134 | }) 135 | } 136 | 137 | func BenchmarkConcat3Builder(b *testing.B) { 138 | benchmarkConcat(b, func(xs []string) string { 139 | var builder strings.Builder 140 | builder.Grow(len(xs[0]) + len(xs[1]) + len(xs[2])) 141 | builder.WriteString(xs[0]) 142 | builder.WriteString(xs[1]) 143 | builder.WriteString(xs[2]) 144 | return builder.String() 145 | }) 146 | } 147 | 148 | func BenchmarkConcat3(b *testing.B) { 149 | benchmarkConcat(b, func(xs []string) string { 150 | return Strings3(xs[0], xs[1], xs[2]) 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package concat_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Quasilyte/concat" 7 | ) 8 | 9 | func ExampleStrings() { 10 | v := "world!" 11 | fmt.Println(concat.Strings("hello, ", v)) 12 | 13 | // Output: hello, world! 14 | } 15 | --------------------------------------------------------------------------------