- Notifications
You must be signed in to change notification settings - Fork0
Ane (atomics and ets) is a library to share mutable data efficiently by utilizing atomics and ets modules.
License
gyson/ane
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Ane (atomics and ets) is a library to share mutable data efficiently byutilizingatomics andets 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 topersistent_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 inmix.exs
:
defdepsdo[{:ane,"~> 0.1.1"}]end
API reference can be found athttps://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"):okiex(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"):okiex(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 specifymode: :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"):okiex(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"):okiex(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.
Theread_concurrency
andwrite_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 aboutread_concurrency
andwrite_concurrency
aterlang ets docs.
Benchmarking script is available atbench/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.exsOperating System: macOS"CPU Information: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHzNumber of Available Cores: 8Available memory: 16 GBElixir 1.7.4Erlang 21.2Benchmark suite executing with the following configuration:warmup: 2 stime: 10 smemory time: 0 μsparallel: 16inputs: none specifiedEstimated total run time: 24 sBenchmarking 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 mssize=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 msComparison:size=16, mode=ane, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 26.76size=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.exsOperating System: macOS"CPU Information: Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHzNumber of Available Cores: 8Available memory: 16 GBElixir 1.7.4Erlang 21.2Benchmark suite executing with the following configuration:warmup: 2 stime: 10 smemory time: 0 μsparallel: 16inputs: none specifiedEstimated total run time: 24 sBenchmarking 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 mssize=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 msComparison:size=1, mode=ane, Ane.get=90%, Ane.put=10.0%, read_concurrency=true, write_concurrency=true, info_size=100 27.03size=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 runningAne.put
is interrupted (e.g. by:erlang.exit(pid, :kill)
), garbagedata could be generated if it finished insert operation but did not start delete operation. Thesegarbabge data could be removed by callingAne.clear
(periodically if it needs to handle constantly interruptions).
# type check with dialyzermix dialyzer# type check with ex_typemixtype
MIT