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.