DEV Community 👩‍💻👨‍💻

Cover image for Make you Go code work 1.5x faster OR even more
Ushakov Michael for Wissance

Posted on

Make you Go code work 1.5x faster OR even more

Introduction

Performance is a key thing of everything, we (humans) don't like to wait and waste our time. Therefore, sometimes quick solution as most of the managers suppose are better than slow 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 convenient on our view way. These not only formatting function do what fmt.Sprintf does in more convenient way but also provide an 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 how to make any golang code work faster.

Parameters and return values

There are two options to pass either arguments 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
Enter fullscreen mode Exit fullscreen mode

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

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

Therefore, the most optimal variant is the 1st variant.

Strings

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

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

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

There is a better solution for string concat - use strings.Builder, but for a better performance you should initially enlarge buffer (using Grow function) to prevent it from some / all re-allocs, but if 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))
Enter fullscreen mode Exit fullscreen mode

Cycles

There are 2 ways to iterate over collection:

  • using range expression
  for _, arg := range args {
      // do some staff here
  }
Enter fullscreen mode Exit fullscreen mode
  • using traditional for with 3 expressions:
  for i := start; i < templateLen; i++ {
      // do some staff here
  }
Enter fullscreen mode Exit fullscreen mode

2nd variant is better by performance. But there are additional advantages of usage for with 3 expressions:

  • reduce 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 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
  }
Enter fullscreen mode Exit fullscreen mode

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:

Image description
. We also suggest you to use our library instead of fmt.Sprintf, because if you prepare strings for sql queries or something like this, it is important to make them as faster, as it possible. We would be thankful if you give us a star on Github and start to follow our organization here and on Github too.

Top comments (2)

Collapse
 
marcello_h profile image
Marcelloh

It is an interesting topic, however I would say with the SQL query example, the real bottleneck is not the one time string conversion, but getting the real data back.
If I look at the difference from your benchmark, it's not that much per operation.
For the 6args version: 53 ns which is perhaps only a dealbreaker when you do massive string conversion.
It would really be beneficial, is you could use this solution exactly the way the fmt version is, which would make it interchangeable.

Collapse
 
evillord666 profile image
Ushakov Michael

@Marcelloh, thanks for your reply. I agree that when we deal with SQL data read is a bottleneck, but anyway it is nice to have strings processing as faster, as it is possible. We also add in our performance test FormatComplex function which takes 7 arguments from map and it 2 times faster than fmt.Sprintf (454 ns). In addition, IMHO we have more convenient arguments positioning instead of placing a lot of %s, %d. When i only start to program C++ i don't like a set of C-printing function and always use std::stream and stream data manipulation. We don't say that fmt is bad, but maybe some of the people find our lib closer neither than fmt.

Here is a post you might want to check out:

Regex for lazy developers

regex for lazy devs

Sorry for the callout 😆