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.
Top comments (3)
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.
@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.@marcello_h , finally
sf
of version1.2.0
has argument formatting feature, therefore i could saysf
andfmt
are interchangeable (see dev.to/wissance/format-a-text-in-g...)