Ane (atomics and ets) is a library to share mutable data efficiently by utilizing atomics and ets modules.
- It stores all data with versionstamp in ETS table.
- It keeps a cached copy with versionstamp locally.
- It uses atomics to save latest versionstamp and syncs data between ETS table and local cache.
- Read operation would use cached data if cache hits and fallback to ETS lookup if cache expires.
- Write operation would update ETS table and versionstamp in atomics array.
Similar to atomics standalone,
- Ane's read/write operations guarantee atomicity.
- Ane's read/write operations are mutually ordered.
- Ane uses one-based index.
Compare to atomics standalone,
- Ane could save arbitrary term instead of 64 bits integer.
Compare to ETS standalone,
-
Ane has much faster read operation when cache hit (this is common for read-heavy application).
- It needs 1 Map operation and 1 atomics operation.
- It does not need to copy data from ETS table.
- It does not need to lookup from ETS table, which could make underneath ETS table's write operation faster.
- Benchmarking showed that it's 2 ~ 10+ times faster.
-
Ane could have slightly slower read operation when cache missed or expired.
- It needs 2 Map operations, 1+ atomics operations and 1+ ETS operations.
- Ane could be faster for "hot key" case.
-
Ane could have slower write operation.
- It needs to do 2 ETS operations and 2+ atomics operations.
- Ane could be faster for "hot key" case.
-
Ane has much faster read/write operations for "hot key" case.
- ETS table performance degrades when a key is too hot due to internal locking.
- Ane avoids "hot key" issue by distributing read/write operations to different keys in underneath ETS table.
-
Ane only supports
:atomics
-like one-based index as key.- I feel it's possible to extend it to be
:ets
-like arbitrary key with some extra complexity. But I do not have that need at the moment.
- I feel it's possible to extend it to be
Compare to persistent_term,
-
Like persistent_term, Ane's read operation with cache hit is lock-free and copying-free (no need to copy since data exists in local cache).
-
Unlike persistent_term, Ane's read operation with cache miss/expire would require copy data from ETS table to the heap of current process.
-
Unlike persistent_term, Ane's write operation is fast and won't trigger global GC.
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.
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"
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.
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 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
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).
# type check with dialyzer
mix dialyzer
# type check with ex_type
mix type
MIT