`fmt.Sprintf` vs string concatenation

Greetings! I was testing a simple function and noticed that the benchmarks for fmt.Sprintf seem much slower as opposed to just concatenating the strings. I suppose it’s doing some more stuff under the hood. The memory benchmarks also seem worse. Am I reading the benchmarks wrong or misunderstanding them? Should I try to avoid using fmt.Sprintf for simple concatenations? Is this a negligible micro optimization that I don’t need to worry about? Thanks!

fmt.Sprintf("One for %v, one for me.", name)

// BenchmarkShareWith-12            2779749               428.4 ns/op           128 B/op          6 allocs/op
"One for " + name + ", one for me."

// BenchmarkShareWith-12           20134160                59.12 ns/op            0 B/op          0 allocs/op

Formatting strings is slower. The arguments to Sprintf (or Printf, Fprintf, etc.) have to be wrapped into interface{}s, then put into an []interface{} slice, then the format string has to be parsed for formatting directives, an underlying buffer has to be created, and then the parsed format string is written into it (e.g. "One for ", then the %v directive is evaluated with the name parameter’s value. name is checked at runtime to be of type string (I assume) and is written to the buffer, followed by ", one for me."). Then the buffer is copied into a new string and returned.

If you’re writing base-10 representations of numbers, formatting time.Time, or other custom data types, the overhead might be worth it. If you have two strings you want to concatenate, definitely just add them together. For 3 or more, I usually put this function into my code:

func concat(strs ...string) string { return strings.Join(strs, "") }

And then use it like result := concat("One for ", name, ", one for me.")

In your specific case, based on the 0 allocs/op, my first guess is that the compiler knew the value of name at compile time (is name a constant or only assigned from a constant a few lines earlier in the code?) and turned your concatenated string into a single constant.

Thanks for shedding light on that, makes total sense.

My functions look like this:

// concat ver:
func ShareWith(name string) string {
	if name == "" {
		name = "you"
	}

	return "One for " + name + ", one for me."
}

// fmt ver:
func ShareWith(name string) string {
	if name == "" {
		name = "you"
	}

	fmt.Sprintf("One for %v, one for me.", name)
}

// benchmark test (from exercism go track https://exercism.io/my/tracks/go):
var tests = []struct {
	name, expected string
}{
	{"", "One for you, one for me."},
	{"Alice", "One for Alice, one for me."},
	{"Bob", "One for Bob, one for me."},
}
func BenchmarkShareWith(b *testing.B) {
	for i := 0; i < b.N; i++ {

		for _, test := range tests {
			ShareWith(test.name)
		}

	}
}

So the compiler is doing some auto optimizations in the the string concatenation version that it can’t do in the fmt version?

Thanks so much for the help!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.