How to Object Pooling in Golang

Updated At Sat, 10 Sep 2022 14:44:24 GMT
Pool of bottles<br>

Today we are going to talk about object pooling. It's a technique that is commonly used in video game industry since it's easy to implement and might tremendously. But let's try to understand it first before start implementing it.

What is Object Pooling?

It's no surprise that the first time you google "What is object pooling" the search result will come from a game engine documentation. So because it comes from the industry that heavily use it (almost always) i will just use their definition.

Object Pooling is a way to optimize your projects and lower the burden that is placed on the CPU when having to rapidly create and destroy new objects. It is a good practice and design pattern to keep in mind to help relieve the processing power of the CPU to handle more important tasks and not become inundated by repetitive create and destroy calls.
- Object Pooling (Unity)

So based on their definition it is best used when object is rapidly created and destroyed. The key term in here is repetitive create and destroy. Object pooling works by basically reusing the created object and mark object as unused instead of letting it be destroyed or garbage collected. But you might be wondering how much performance you can get by using object pooling before you implement it right? I mean added complexity without major benefit isn't worth it right? So let's look at the benefit.

Benefit of Object Pooling

Since we know that object pooling basically just reuse the object again instead of destroying it, we can expect that major performance boost came from memory intensive task. So let's create a benchmark for it. Below are the code we will use for the benchmark :

package%20pool%0A%0Aimport%20(%0A%09%22bytes%22%0A%09%22strconv%22%0A%09%22sync%22%0A%09%22testing%22%0A)%0A%0Afunc%20BaseExecution(buffer%20*bytes.Buffer)%20string%20%7B%0A%09for%20i%20:=%200;%20i%20%3C%20100;%20i++%20%7B%0A%09%09buffer.WriteString(%22Value%20:%20%22%20+%20strconv.Itoa(i))%0A%09%7D%0A%09return%20buffer.String()%0A%7D%0A%0Afunc%20BenchmarkUnpooled(b%20*testing.B)%20%7B%0A%09b.RunParallel(func(p%20*testing.PB)%20%7B%0A%09%09for%20p.Next()%20%7B%0A%09%09%09bytesStuff%20:=%20bytes.NewBuffer(nil)%0A%09%09%09_%20=%20BaseExecution(bytesStuff)%0A%09%09%7D%0A%09%7D)%0A%7D%0A%0Afunc%20BenchmarkPooled(b%20*testing.B)%20%7B%0A%09pool%20:=%20sync.Pool%7B%0A%09%09New:%20func()%20any%20%7B%0A%09%09%09return%20bytes.NewBuffer(nil)%0A%09%09%7D,%0A%09%7D%0A%0A%09b.RunParallel(func(p%20*testing.PB)%20%7B%0A%09%09for%20p.Next()%20%7B%0A%09%09%09bytesStuff%20:=%20pool.Get().(*bytes.Buffer)%0A%09%09%09_%20=%20BaseExecution(bytesStuff)%0A%09%09%09bytesStuff.Reset()%0A%09%09%09pool.Put(bytesStuff)%0A%09%09%7D%0A%09%7D)%0A%7D%0A

And below are the benchmark result of with and without object pooling :

Name Ops Time/Op Memory Allocation
BenchmarkUnpooled-16 1403676 854.2 ns/op 3008 B/op 6 allocs/op
BenchmarkPooled-16 2384380 503.3 ns/op 1024 B/op 1 allocs/op

Look at that, by just adding a code to do the pooling we just got an increase of 69%! We can also see that it uses less memory and perform less allocation. This is because the object is being reused when it's finished executing other task. This is a huge when we deal with performance critical system such as games as every millisecond counts.  Also, let's analyze the code difference shall we?

In the unpooled code, we just spawn the object on demand and do our operation. After we finish the operation we just ignore it and let the garbage collector do it's job. Meanwhile, in the pooled code, we define the container (sync.Pool) and then tell it how to build the new object if needed (this should only happen when there is no object in the pool). Also, we have another responsibility to reset the object state to it's clean slate before putting it back since it might be used by other execution.

Sometimes your favorite language might not yet implement it and you have to implement it yourself. So we are going to implement crude version of it.

How to Implement Object Pooling

Well, to build a simple object pooler we only need 2 things, synchronization for multi-threading support and a data structure that can grow to arbitrary size. For multi-threading purpose we will just use mutex provided by the sync package. And for the data structure we will just use plain old linked list, though you are free to use other data structure if you want (queue or stack maybe?). We also need to define at least 2 function which GET and PUT though you are free to use different words if you want. And below are the sample code for it :

type%20SimplePooler%20struct%20%7B%0A%09onNewObj%20%20func()%20any%0A%09mutex%20%20%20%20%20*sync.Mutex%0A%09container%20*list.List%0A%7D%0A%0Afunc%20(s%20*SimplePooler)%20Get()%20any%20%7B%0A%09s.mutex.Lock()%0A%09defer%20s.mutex.Unlock()%0A%09if%20front%20:=%20s.container.Front();%20front%20!=%20nil%20%7B%0A%09%09return%20s.container.Remove(front)%0A%09%7D%0A%09return%20s.onNewObj()%0A%7D%0A%0Afunc%20(s%20*SimplePooler)%20Put(obj%20any)%20%7B%0A%09s.mutex.Lock()%0A%09defer%20s.mutex.Unlock()%0A%09s.container.PushBack(obj)%0A%7D%0A%0Afunc%20NewSimplePooler(onEmpty%20func()%20any)%20*SimplePooler%20%7B%0A%09return%20&SimplePooler%7B%0A%09%09onNewObj:%20%20onEmpty,%0A%09%09mutex:%20%20%20%20%20&sync.Mutex%7B%7D,%0A%09%09container:%20list.New(),%0A%09%7D%0A%7D%0A%0Afunc%20BenchmarkCrudePooled(b%20*testing.B)%20%7B%0A%09pool%20:=%20NewSimplePooler(func()%20any%20%7B%0A%09%09return%20bytes.NewBuffer(nil)%0A%09%7D)%0A%0A%09b.RunParallel(func(p%20*testing.PB)%20%7B%0A%09%09for%20p.Next()%20%7B%0A%09%09%09bytesStuff%20:=%20pool.Get().(*bytes.Buffer)%0A%09%09%09_%20=%20BaseExecution(bytesStuff)%0A%09%09%09bytesStuff.Reset()%0A%09%09%09pool.Put(bytesStuff)%0A%09%09%7D%0A%09%7D)%0A%7D%0A

And here is the benchmark result :

Name Ops Time/Op Memory Allocation
BenchmarkUnpooled-16 1431043 833.2 ns/op 3008 B/op 6 allocs/op
BenchmarkPooled-16 2372421 493.0 ns/op 1024 B/op 1 allocs/op
BenchmarkCrudePooled-16 2089744 561.5 ns/op 1072 B/op 2 allocs/op

Well, looking at the benchmark it wasn't as good as what golang already provide, but it still beats the unpooled version.

Remarks

Well, i think that's all i can share about object pooling and let's close it with remarks.

  • The object may uses lot's of memory.
  • The object will be reuse-able and reset-able.
  • The object is created and reused repetitively.

That's all thank you~

Reference

  • Photo : https://www.pexels.com/photo/assorted-plastic-bottles-802221/
  • Golang Sync Package : https://pkg.go.dev/sync