Go optimization checklist

Bunch of tips for Go optimization. Very useful when you are looking for performance improvements.

Go optimization checklist

Useful information about Go optimization. It has been created as reminder of possible options.

Verify If you need optimization

It is important to say that optimization is not always necessary, if we have got service which works well and enough then optimization is not that need. Consider optimization when you have performance requirements or your code must be more efficient.

Skip unnecessary logic

Simple and easy to say but really sometimes we do unnecessary work. For example, we convert one type to another only for use some special function (try to implement that function for you specific type) or we do unnecessary check which will never occur.

Check used generic/libraries function

Functions form libraries or writen for generic purpose have a lot of checks and unnecessary code. If you are sure about your data and people you work with then you can reimplement that function by yourself.

Don’t repeat

Loops and validators are the most often repeated things. Verify that when you will be looking for optimizations.

Math knowledge

Calculation often uses loops and step by step functions. Math offers sometimes very good solutions for our problems. One big example are matrix which might be used for optimize many things related with math functions.

Precompute and save results

Often we recalculate given algorithm hundreds of times instead of save the results once and use them again. Ofcourse, sometimes it is not profitable, but sometimes it might reduce our CUP usage (but memory will be needed).

Caching

Just caching. Not only for HTTP requests or database queries. Sometimes regular functions or method can be cached. Remember that you trade there your CPU for memory.

Augment data structure

Sometimes we can add some additional variable to our processing structure which is available during computing or processing and can be used in the future. Otherwise, we have to recalculate it again and again to get desired results.

Decompressing

It requires a lot of CPU to process. Sometimes trade for memory might be profitable.

Reduce Allocation

Just reduce the number of objects you puts on the heap. This will improve GC performance (less memory to clean and less work). Check type conversions ([]byte to string) or check if object must go to heap.

Reuse Memory

Sometimes it’s ok to not clean up after some processes when you know that you will reuse it. Good example is network connection or big structures allocation. Unfortunately state reset or concurrent usage of that memory might be complex.

Optimize allocated object

Simple reduce pointers in allocated object. It is rare possible and sometimes it might require outstanding ideas like changing []string into offsets []int and bytes []byte. It will reduce number of pointers because every string has pointer to []byte.

Being L-cache friendly

To make Go programs more L-cache friendly, use contiguous memory layouts like slices and arrays instead of maps or linked lists. Structure data in a way that aligns with your access patterns, such as preferring Structure of Arrays (SoA) over Array of Structures (AoS) for better cache utilization. Access data sequentially to leverage cache line efficiencies and minimize cache misses. Additionally, pre-allocate slices to avoid frequent reallocations, and align data to cache line boundaries to prevent false sharing between goroutines.

GC tuning

Go GC has two config options: GOGC and GOMEMLIMIT. Unfortunately, it requires lots of benchmarking to find the right number. The GOMEMLIMIT option is designed to help if the GC memory overhead is causing OOMs (GC reacted to memory spikes). Setting the GOMEMLIMIT option to 90–95% of the workload memory limit might be quite effective.

Triggering GC

Sometimes anti-pattern runtime.GC() might be helpful.

Allocating objects off-heap

There is possibility to allocate memory not on heap or stack but on off-heap (outside the Go runtime’s responsibility). Doing that you reduce pressure on the garbage collector and improve performance

Check Resources Leaking/Wasting

It is simple as it sounds. Profile and benchmark your code to find that.

Control your Goroutines

Goroutines might be tricky. Watch your code and use good pattern when you deal with them. Test your code against goroutines leak (goleak tool)

Close resources if possible

Just it.

Pre-Allocate If You Can

Using make (slices and maps) with proper params and .Grow method for buffers, etc.

Understand io package

You can use different functions for the same job. For example ReadAll and ReadFull. Each of them might affect your performance differently.

Consider pooling

Sometimes pooling might optimize your code execution but sometimes also it can introduce memory consumption issues. You can use sync.Pool which have very specific use cases. Use it when:

  • You want to reuse large or extreme amounts of objects to reduce the latency of those allocations.
  • You don’t care about the object content, just its memory blocks.
  • You want to reuse those objects from multiple goroutines, which can vary in number.
  • You want to reuse objects between quick computations that frequently happen (maximum one GC cycle away).

Structure size optimization

To optimize the size of structs in Go, arrange fields from largest to smallest to minimize padding and ensure better memory alignment. Group smaller fields together to reduce the amount of padding needed. There are analyzers for that:

Help CPU branch prediction

Improving CPU branch prediction in Go involves writing code that reduces branch predictions, which can significantly enhance performance. This is quite complex topic and requires deep knowledge about CPU architecture and Go compiler.

Sources

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy