Using constant pools to speed up your Elixir code

Jacob Lerche
Nov 22, 2017 · 6 min read
Image for post
Image for post
A nice picture of a pool.

Introduction

Writing code in Elixir that is optimized for speed can sometimes be challenging due to the nature of the Erlang runtime environment’s (BEAM) concurrency model. Instead of using OS threads like, say, Java and other languages, BEAM provides its own abstraction in the form of processes implemented in the runtime environment itself. These processes have their own stack and heap, but more importantly they share nothing with other processes. Communication between processes is done via message passing. In practice what this means is that any message, which is just an Erlang term, that is sent to another process must be copied to the destination’s heap.

A naive GenServer constant store

To get a feel for the problem, let us start with a contrived scenario: we wish to store some data to be available to other processes. We will naively do so with a GenServer like in the following example.

ETS tables as a first optimization

Erlang Term Storage (ETS) is an in-memory data store with some modest concurrency guarantees, namely that updates to single objects are both atomic and isolated. What this means is that any update to an object either succeeds or fails without effect, and no other processes will see intermediate results of the update. An ETS table is created from within a process and is stored off-heap. The process maintains ownership and the table persists until its owner process dies.

Module constant pools

Constant Erlang terms are also called literals and are treated in a somewhat special manner. Per the Erlang docs efficiency guide in section 8.2, the literals are stored in that module’s pool. These pools are heaps in their own right, but may actually be referenced directly from other processes without being copied. This implementation detail can be quite useful for alleviating memory pressure issues. So we can solve both process read access and data copying problems in our scenario with the following.

defmodule StoreSetter do
defmacro set(val) do
quote do
defmodule Store do
def get() do
unquote(val)
end
end
end
end
end
iex(1)> c "naive_dynamic_module.ex"
[StoreSetter]
iex(2)> require StoreSetter
StoreSetter
iex(3)> StoreSetter.set([:foo])
{:module, Store,
<<70, 79, 82, 49, 0, 0, 3, 140, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 82,
0, 0, 0, 8, 12, 69, 108, 105, 120, 105, 114, 46, 83, 116, 111, 114, 101, 8,
95, 95, 105, 110, 102, 111, 95, 95, 9, ...>>, {:get, 0}}
iex(4)> Store.get
[:foo]
iex(5)> StoreSetter.set([:foo, :bar])
warning: redefining module Store (current version defined in memory)
iex:5
{:module, Store,
<<70, 79, 82, 49, 0, 0, 3, 152, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 82,
0, 0, 0, 8, 12, 69, 108, 105, 120, 105, 114, 46, 83, 116, 111, 114, 101, 8,
95, 95, 105, 110, 102, 111, 95, 95, 9, ...>>, {:get, 0}}
iex(6)> Store.get
[:foo, :bar]
iex(7)> :code.purge(Store)
false
iex(8)> :code.delete(Store)
true
iex(9)> StoreSetter.set([:foo, :bar, :baz])
{:module, Store,
<<70, 79, 82, 49, 0, 0, 3, 156, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 82,
0, 0, 0, 8, 12, 69, 108, 105, 120, 105, 114, 46, 83, 116, 111, 114, 101, 8,
95, 95, 105, 110, 102, 111, 95, 95, 9, ...>>, {:get, 0}}

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store