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
According to our performance tests, we could conclude the following:
- We’ve got small performance rise when we pass arguments by pointer because we’ve got rid of arguments copying.
- 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)
}
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))
Cycles
There are 2 ways to iterate over collection:
- using
range
expression
for _, arg := range args {
// do some staff here
}
- using traditional
for
with 3 expressions:
for i := start; i < templateLen; i++ {
// do some staff here
}
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
}
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:
. 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.