Make Your Go Code Work 1.5x Faster or Even More – DZone Web Dev


Performance is a key thing in everything; we (humans) don’t like to wait and waste our time. Therefore, sometimes quick solutions, as most of the managers suppose, are better than slow ones but with good engineering and design. But today, we are not speaking about management but about code performance. We have a small text formatting library that allows us to format text using a template in a convenient on our view way. These not only formatting functions do what fmt.Sprintf does this in a more convenient way but also provides additional features. Previous versions of our module were loose in performance to fmt.Sprintf, but since 1.0.1, we are better. And today, we are going to tell you how to make any golang code work faster.

Parameters and Return Values

There are two options to pass either argument and/or get function result – by pointer and by value; consider the following example:

func getItemAsStr(item *interface{}) string   // 1st variant
func getItemAsStr(item interface{})  string   // 2nd variant
func getItemAsStr(item *interface{}) *string  // 3rd variant
func getItemAsStr(item interface{})  *string  // 4th variant

According to our performance tests, we could conclude the following:

  1. We’ve got a small performance rise when we pass arguments by pointer because we’ve got rid of arguments copying.
  2. We’ve got a performance decrease when we return a pointer to the function local variable.

Therefore, the most optimal variant is the first variant.

Strings

Strings are immutable like in many other programming languages; therefore, string concatenation using the + operator is a bad idea, i.e. code like this is very slow:

var result string = ""
for _, arg := range args {    result += getItemAsStr(&arg)
}

Every “+” creates a new string object; as a result, memory usage rise, and performance significantly decreases due to spending sufficient time on allocations to new variables.

There is a better solution for string Concat —  use strings.Builder, but for better performance, you should initially enlarge the buffer (using Grow function) to prevent it from some / all re-allocs, but if the buffer size will be very large, there will be a penalty to performance due to initial memory allocation will be slow; therefore you should choose initial buffer size wisely, i.e.:

var formattedStr = &strings.Builder{}
formattedStr.Grow(templateLen + 22*len(args))

Cycles

There are two ways to iterate over the collection:

  for _, arg := range args {      // do some staff here  }

  • Using traditional for with three expressions:
  for i := start; i < templateLen; i++ {      // do some staff here  }

The second variant is better in performance. But there are additional advantages of usage for three expressions:

  • Reduce the number of iterations; more iteration code is slower; therefore, if you could affect your loop value, do it. This can’t be achieved with range;
  • Set the initial value for iteration much higher if it is possible, i.e. in our case:
  start := strings.Index(template, "{")  if start < 0 {     return template  }
  formattedStr.WriteString(template[:start])  for i := start; i < templateLen; i++ {      // iterate over i  }

Conclusion

Using such simple techniques, we made our code run 1.5 times faster than it was, and now it works faster even than fmt.Sprintf, see our performance measurements:

Quoted from Various Sources

Published for: The Bloggers Briefing