DEV Community

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

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 +.