Ane

Ane (atomics and ets) is a library to share mutable data efficiently by utilizing atomics and ets modules.

How it works ?

Properties

Similar to atomics standalone,

Compare to atomics standalone,

Compare to ETS standalone,

Compare to persistent_term,

Installation

Note: it requires OTP 21.2 for :atomics, which was released on Dec 12, 2018.

It can be installed by adding :ane to your list of dependencies in mix.exs:

def deps do
  [
    {:ane, "~> 0.1.1"}
  ]
end

API reference can be found at https://hexdocs.pm/ane/Ane.html.

Usage

iex(1)> a = Ane.new(1)
{#Reference<0.376557974.4000972807.196270>,
 #Reference<0.376557974.4000972807.196268>,
 #Reference<0.376557974.4000972807.196269>, %{}}
iex(2)> Ane.put(a, 1, "hello")
:ok
iex(3)> {a, value} = Ane.get(a, 1)
{{#Reference<0.376557974.4000972807.196270>,
  #Reference<0.376557974.4000972807.196268>,
  #Reference<0.376557974.4000972807.196269>, %{1 => {1, "hello"}}}, "hello"}
iex(4)> value
"hello"
iex(5)> Ane.put(a, 1, "world")
:ok
iex(6)> {a, value} = Ane.get(a, 1)
{{#Reference<0.376557974.4000972807.196270>,
  #Reference<0.376557974.4000972807.196268>,
  #Reference<0.376557974.4000972807.196269>, %{1 => {2, "world"}}}, "world"}
iex(7)> value
"world"

Compare Ane and ETS Standalone

Generally, Ane is faster for read-heavy case and ETS standalone is faster for write-heavy case. This library provide a way to switch between them seamlessly.

By specify mode: :ets as following, it will use ETS standalone instead:

iex(1)> a = Ane.new(1, mode: :ets)
{#Reference<0.2878440188.2128478212.58871>, 1}
iex(2)> Ane.put(a, 1, "hello")
:ok
iex(3)> {a, value} = Ane.get(a, 1)
{{#Reference<0.2878440188.2128478212.58871>, 1}, "hello"}
iex(4)> value
"hello"
iex(5)> Ane.put(a, 1, "world")
:ok
iex(6)> {a, value} = Ane.get(a, 1)
{{#Reference<0.2878440188.2128478212.58871>, 1}, "world"}
iex(7)> value
"world"

This is useful for comparing performance between Ane and ETS standalone.

Performance Tuning

The read_concurrency and write_concurrency from ETS table are important configurations for performance tuning. You can adjust it while creating Ane instance like following:

ane = Ane.new(1, read_concurrency: true, write_concurrency: true)

These options would be passed to underneath ETS table. You can read more docs about read_concurrency and write_concurrency at erlang ets docs.

Benchmarking

Benchmarking script is available at bench/comparison.exs.

Following is the benchmarking result for comparing Ane and ETS standalone with 90% read operations and 10% write operations:

$ mix run bench/comparison.exs
Operating System: macOS"
CPU Information: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz
Number of Available Cores: 8
Available memory: 16 GB
Elixir 1.7.4
Erlang 21.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 10 s
memory time: 0 μs
parallel: 16
inputs: none specified
Estimated total run time: 24 s


Benchmarking size=16, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100...
Benchmarking size=16, mode=ets, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100...

Name                                                                                                                   ips        average  deviation         median         99th %
size=16, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100         26.76       37.37 ms    ±37.32%       36.79 ms       72.50 ms
size=16, mode=ets, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100          9.66      103.55 ms    ±37.82%       98.66 ms      187.74 ms

Comparison:
size=16, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100         26.76
size=16, mode=ets, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100          9.66 - 2.77x slower

Following is the benchamrking result for comparing Ane and ETS standalone for "hot key" issue:

$ mix run bench/comparison.exs
Operating System: macOS"
CPU Information: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz
Number of Available Cores: 8
Available memory: 16 GB
Elixir 1.7.4
Erlang 21.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 10 s
memory time: 0 μs
parallel: 16
inputs: none specified
Estimated total run time: 24 s


Benchmarking size=1, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100...
Benchmarking size=1, mode=ets, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100...

Name                                                                                                                  ips        average  deviation         median         99th %
size=1, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100         27.03       37.00 ms    ±45.40%       36.15 ms       71.12 ms
size=1, mode=ets, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100          1.33      754.31 ms    ±25.91%      762.88 ms     1212.87 ms

Comparison:
size=1, mode=ane, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100         27.03
size=1, mode=ets, Ane.get=90%, Ane.put=10.0%,  read_concurrency=true, write_concurrency=true, info_size=100          1.33 - 20.39x slower

Handling Garbabge Data in Underneath ETS table

Write operation (Ane.put) includes one :ets.insert operation and one :ets.delete operation. When the process running Ane.put is interrupted (e.g. by :erlang.exit(pid, :kill)), garbage data could be generated if it finished insert operation but did not start delete operation. These garbabge data could be removed by calling Ane.clear (periodically if it needs to handle constantly interruptions).

License

MIT