DEV Community

Concatenate strings in golang a quick benchmark : + or fmt.Sprintf ?

pmalhaire on February 08, 2019

Concatenate strings in golang a quick benchmark Introduction When I begin to enter golang code bases with developer of variou...
Collapse
 
fasmat profile image
Matthias Fasching • Edited

Your benchmark is incorrect, the only thing you are measuring is the optimizations of the golang compiler / runtime in simple cases. Here is a slightly improved benchmark that shows how hugely different results you get if you disable golang optimizations (or make it hard for go to optimize your code):

package test

import (
    "fmt"
    "strings"
    "testing"
)

var str, longStr string = "my_string", `qwertyuiopqwertyuiopqwertyuio
qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiop`

const cStr = "my_string"

var result string

func BenchmarkPlus(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s += str
    }
    result = s
}

func BenchmarkLongPlus(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s += longStr
    }
    result = s
}

func BenchmarkConstPlus(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s += cStr
    }
    result = s
}

func BenchmarkJoin(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s = strings.Join([]string{s, str}, "")
    }
    result = s
}

func BenchmarkLongJoin(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s = strings.Join([]string{s, longStr}, "")
    }
    result = s
}

func BenchmarkConstJoin(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s = strings.Join([]string{s, cStr}, "")
    }
    result = s
}
func BenchmarkSprintf(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s = fmt.Sprintf("%s %s", s, str)
    }
    result = s
}

func BenchmarkLongSprintf(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s = fmt.Sprintf("%s %s", s, longStr)
    }
    result = s
}

func BenchmarkConstSprintf(b *testing.B) {
    var s string
    for n := 0; n < b.N; n++ {
        s = fmt.Sprintf("%s %s", s, cStr)
    }
    result = s
}

func BenchmarkBuilder(b *testing.B) {
    var sb strings.Builder
    for n := 0; n < b.N; n++ {
        sb.WriteString(str)
    }
    result = sb.String()
}

func BenchmarkLongBuilder(b *testing.B) {
    var sb strings.Builder
    for n := 0; n < b.N; n++ {
        sb.WriteString(longStr)
    }
    result = sb.String()
}

func BenchmarkConstBuilder(b *testing.B) {
    var sb strings.Builder
    for n := 0; n < b.N; n++ {
        sb.WriteString(cStr)
    }
    result = sb.String()
}
Enter fullscreen mode Exit fullscreen mode

If executed with go test -gcflags=-N -bench=. returns the following results:

BenchmarkPlus-4                   114433            119694 ns/op
BenchmarkLongPlus-4                10000            118300 ns/op
BenchmarkConstPlus-4              122350            128256 ns/op
BenchmarkJoin-4                    91172            122361 ns/op
BenchmarkLongJoin-4                10000            142659 ns/op
BenchmarkConstJoin-4               86749            114199 ns/op
BenchmarkSprintf-4                 57369            152416 ns/op
BenchmarkLongSprintf-4             10000            268300 ns/op
BenchmarkConstSprintf-4            47094            139441 ns/op
BenchmarkBuilder-4              29206484                77.87 ns/op
BenchmarkLongBuilder-4           8734220              1438 ns/op
BenchmarkConstBuilder-4         37201794                41.32 ns/op
PASS
Enter fullscreen mode Exit fullscreen mode

As you can see a Builder is often more than 1000x faster than other approaches. fmt.Sprintf and strings.Join have about the same speed as +, but this changes as soon as you do multiple concatenations in a single call:

s := "string1" + "string2" + "string3"
s := strings.Join([]string{"string1", "string2", "string3"})
Enter fullscreen mode Exit fullscreen mode

here strings.Join will be measurable faster than +.

Collapse
 
bgadrian profile image
Adrian B.G.

I tried to do this micro optimization a few months ago and kinda failed.

The recommended way is to use strings.Builder. As I did not knew the string size a simple + worked better in benchmarks (at least for strings less than ~20 characters.

I ended up approximating the result (and pre allocate memory with a buffer) and got the best result, but most of the times + is the best choice.

Collapse
 
fasmat profile image
Matthias Fasching • Edited

+ will always be the slowest way to concatenate strings.

In simple cases (concatenate only exactly 2 strings) every other method: builder, join, and sprintf will be ~ the same speed as +.

The benchmark here is just incorrect. Because the resulting string in the Plus tests isn't assigned to anything the compiler just makes it a NOP before executing the tests.

Run the benchmarks again and disable optimizations (go test -gcflags=-N -bench=.) and you will see that all methods have ~ the same execution time. In cases where you concatenate more than 2 strings + will always be the slowest (and most memory hungry) method.

Collapse
 
pmalhaire profile image
pmalhaire

Thanks for your comment I'll update my post accordingly, note that the c version preallocates the buffer.

Collapse
 
titogeorge profile image
Tito George

In my case join wins by a long margin, cant figure out whats wrong.

var word = []string{"9b4e6f7f-1c37-4730-9417-e23747572608", "9b4e6f7f-1c37-4730-9417-e23747572608", "9b4e6f7f-1c37-4730-9417-e23747572608", "9b4e6f7f-1c37-4730-9417-e23747572608", "9b4e6f7f-1c37-4730-9417-e23747572608", "9b4e6f7f-1c37-4730-9417-e23747572608"}

func BenchmarkStringsSprint(b *testing.B) {
    b.ReportAllocs()
    values := ""
    for i := 0; i < b.N; i++ {
        for _, s := range word {
            values = fmt.Sprintf("%s %s", values, s)
        }
    }
}

func BenchmarkStringsJoin(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        values := strings.Join(word, " ")
        _ = values
    }
}

func BenchmarkBuilder(b *testing.B) {
    b.ReportAllocs()
    for n := 0; n < b.N; n++ {
        var b strings.Builder
        for _, s := range word {
            b.WriteString(s)
            b.WriteByte(' ')
        }
        _ = b.String()
    }
}
Enter fullscreen mode Exit fullscreen mode

Join has less allocs as well

BenchmarkStringsSprint
BenchmarkStringsSprint-16                      10000       2180843 ns/op    13404016 B/op         32 allocs/op
BenchmarkStringsJoin
BenchmarkStringsJoin-16                     10398318           139.0 ns/op       224 B/op          1 allocs/op
BenchmarkBuilder
BenchmarkBuilder-16                          4303065           247.6 ns/op       720 B/op          4 allocs/op
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jrwren profile image
Jay R. Wren

On strace: string concatenation isn't a system call. The strace for these programs should be the same as the strace for hello world.

Collapse
 
pmalhaire profile image
pmalhaire

I didn't mean to say that. What made you think this way ? Maybe I Can make my post more clear with your help.

Collapse
 
jrwren profile image
Jay R. Wren

The article is about string concatenation. Why look at strace at all?

Thread Thread
 
pmalhaire profile image
pmalhaire

It's to explain why C is more efficient than Go, which is no explicitly explained.

Thread Thread
 
jrwren profile image
Jay R. Wren

I don't agree that a syscall count has anything to do with a languages efficiency compared to another language.

Thread Thread
 
pmalhaire profile image
pmalhaire

I'll make it more clear.