Qqwy
Arrays - Fast and versatile arrays with swappable implementations
While not as prevalent as in imperative languages, arrays (collections with efficient random element access) are still very useful in Elixir for certain situations. However, so far a stable and idiomatic array library was still missing, meaning that people often had to resort to directly using Erlang’s not-so-idiomatic :array module.
Arrays aims to be this stable, efficient and idiomatic array library.
Arrays
Arrays is a library to work with well-structured Arrays with fast random-element-access for Elixir, offering a common interface with multiple implementations with varying performance guarantees that can be switched in your configuration.
Installation
Arrays is available in Hex and can be installed
by adding arrays to your list of dependencies in mix.exs:
def deps do
[
{:arrays, "~> 2.0"}
]
end
Documentation can be found at https://hexdocs.pm/arrays.
Using Arrays
Some simple examples:
Constructing Arrays
By calling Arrays.new or Arrays.empty:
iex> Arrays.new(["Dvorak", "Tchaikovsky", "Bruch"])
#Arrays.Implementations.MapArray<["Dvorak", "Tchaikovsky", "Bruch"]>
iex> Arrays.new(["Dvorak", "Tchaikovsky", "Bruch"], implementation: Arrays.Implementations.ErlangArray)
#Arrays.Implementations.ErlangArray<["Dvorak", "Tchaikovsky", "Bruch"]>
By using Collectable:
iex> [1, 2, 3] |> Enum.into(Arrays.new())
#Arrays.Implementations.MapArray<[1, 2, 3]>
iex> for x <- 1..2, y <- 4..5, into: Arrays.new(), do: {x, y}
#Arrays.Implementations.MapArray<[{1, 4}, {1, 5}, {2, 4}, {2, 5}]>
Some common array operations:
- Indexing is fast.
- The full Access calls are supported,
- Variants of many common
Enum-like functions that keep the result an array (rather than turning it into a list), are available.
iex> words = Arrays.new(["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"])
#Arrays.Implementations.MapArray<["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]>
iex> Arrays.size(words) # Runs in constant-time
9
iex> words[3] # Indexing is fast
"fox"
iex> words = put_in(words[2], "purple") # All of `Access` is supported
#Arrays.Implementations.MapArray<["the", "quick", "purple", "fox", "jumps", "over", "the", "lazy", "dog"]>
iex> # Common operations are available without having to turn the array back into a list (as `Enum` functions would do):
iex> Arrays.map(words, &String.upcase/1) # Map a function, keep result an array
#Arrays.Implementations.MapArray<["THE", "QUICK", "PURPLE", "FOX", "JUMPS", "OVER", "THE", "LAZY", "DOG"]>
iex> lengths = Arrays.map(words, &String.length/1)
#Arrays.Implementations.MapArray<[3, 5, 6, 3, 5, 4, 3, 4, 3]>
iex> Arrays.reduce(lengths, 0, &Kernel.+/2) # `reduce_right` is supported as well.
36
Concatenating arrays:
iex> Arrays.new([1, 2, 3]) |> Arrays.concat(Arrays.new([4, 5, 6]))
#Arrays.Implementations.MapArray<[1, 2, 3, 4, 5, 6]>
Slicing arrays:
iex> ints = Arrays.new(1..100)
iex> Arrays.slice(ints, 9..19)
#Arrays.Implementations.MapArray<[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]>
Rationale
Algorithms that use arrays can be used while abstracting away from the underlying representation.
Which array implementation/representation is actually used, can then later be configured/compared, to make a trade-off between ease-of-use and time/memory efficiency.
Arrays itself comes with two built-in implementations:
Arrays.Implementations.ErlangArraywraps the Erlang:arraymodule, allowing this time-tested implementation to be used with all common Elixir protocols and syntactic sugar.Arrays.Implementations.MapArrayis a simple implementation that uses a map with sequential integers as keys.
By default, the MapArray implementation is used when creating new array objects, but this can be configured by either changing the default in your whole application, or by passing an option to a specific invocation of new/0-2, or empty/0-1.
iex> words = Arrays.new(["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"])
#Arrays.Implementations.MapArray<["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]>
Is it fast?
I’m proud to finally present a stable release of this library for you all. Work on Arrays started a few years back but was on the backburner because of other projects. Now, I finally had some time to get back to it.
The library is heavily documented, specced and (doc)tested.
I’m very eager to hear your feedback! ![]()
~Marten/Qqwy
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #performance
- #security












First 10 of 14 Posts
cmo
Any performance difference on the two array types with small, medium or large arrays?
Qqwy
This is a great question!
The current roadmap is as follows:
from_rawandto_rawfunctions to ErlangArray to work with pre-existing code that operates on the raw:arrayrecord itself.(potentially based on persistent_vector). → See below.Arraysmodule (swapping elements, sorting, shuffling)Qqwy
I have created a couple of benchmarks, and added them to the README.
Just like all benchmarks, these should be taken with a grain of salt.
There are probably ways to improve them.
EDIT: nicer and more recent graphs can be found a few posts further down.
Benchmarks
You can run the benchmarks locally by running
mix run benchmarks/benchmarks.exs,which will also output the HTML format with nice graphs.
Append a single element
Appending a single element is very fast on arrays, even as sizes grow.
MapArray and ErlangArray perform similarly.
For extra comparison, we look at lists both to see how slow
list ++ [val]becomes as baseline,but also how fast
[val | list]still is:In certain situations where a list can be treated as ‘backwards’, this can be a very simple way to append elements.
As doing this is built-in, it will always be faster than our arrays.
Thus, it serves as a ‘maxline’.
Random element access
Accessing a random element is very fast on arrays, even as sizes grow.
Arrays start beating lists significantly once the collection has more than 256 elements.
MapArray and ErlangArray seem to perform similarly < 8192 elements.
For larger sizes, ErlangArray seems to be a factor ~2 slower than MapArray again.
Random element update
Arrays start beating lists once the collection has more than 128 elements.
For sizes up to 131072 elements, MapArray seems to be between 100% and 30% slower than ErlangArray.
For longer arrays, MapArray wins out, with ErlangArray being ~30% slower.
It seems like
put_inhas some overhead w.r.t. callingArrays.replace.This warrants more investigation. Maybe
Accesshas some overhead for its calls,or maybe the implementations of
get_and_update_incould be further optimized.Concatenate two equally-large collections
Strangely, concatenation of large collections is very fast on lists.
Probably because all of it happens in a single built-in function?
Lists outperform arrays 20x-100x for this task.
Between ErlangArray and MapArray, ErlangArray seems to handle this task 50% faster when concatenating two 4068-element arrays, and twice as fast for larger collections.
From above benchmarks, we know (caveat emptor):
EDIT: Added some graphs to this post.
Qqwy
Version 1.2.0 has been released. It adds
ErlangArray.from_raw/1andErlangArray.to_raw/1for interop with:array-records created/consumed by other code.The next version will probably be already
2.0.0as some pain-points in the current interfaces have been brought to my attention. To be precise:Access.popimplementation, as only popping the last element of the array can be done reasonably fast.emptyas part of the protocol, makingArray.Behavioursuperfluous.:arrayrequires a default value to be set at the start. This default will then be used for all empty elements, also when resizing later. Most other array-implementations do not have this restriction, and allowresizeto take a different default argument each turn. It seems non-sensible to require other array types to also specify the default value at the start (and be unable to change it later) only because:arraydictates this.These changes will make implementing the Arrays Protocol for other datatypes significantly easier
.
paulstatezny
This library seems well thought out!
Maybe it’s just me, but I wish it were called
(List, Enum, Tuple, Integer…Array)
Array(singular) to match the Elixir standard library.Qqwy
Thank you!
Yes, that would have been nice. However, a package with that name was already registered on Hex.PM seven years ago, so there is very little that can be done
.
EDIT: And besides that, having an OTP application (like any Elixir library is) associated with the application atom name
:array(or any other standard-library module for that matter) seems like a rather bad idea as well.And besides this, the plural form was chosen to indicate that the underlying implementation can easily be swapped out
.
Qqwy
I have added some graphs to the benchmarking post above.
Some interesting observations:
Qqwy
Version 2.0.0 has been released!
Improves the
Arrays.Protocolto be more friendly to implement. Specifically:Access.pop. Instead, throw an error when people try to use it.:popis used insideAccess.get_and_updateemptyfromAccess.BehaviourtoAccess.Protocol.:default. It is no longer a required setting, and all arrays are able to work with adefaultpassed toresize.Arrays.Protocol.resize/2withArrays.Protocol.resize/3. (Arrays.resize/2will call it withnilas third parameter).sizeis no longer a required setting.Arrays.new/2andArrays.empty/1have been edited to reflect this.Improved tests, documentation, code examples.
Also, there now is some comprehensive benchmarking code that compares the various array implementations.
Qqwy
I have been experimenting with implementing an Arrays implementation in native code, by wrapping an immutable datastructure written in Rust using NIFs (Natively Implemented Functions).
The result can be found on the ArraysRRBVector repo, or also as the library of the same name which has been published on Hex.PM (v0.1.0, and it is unlikely to be refined into a stable version).
Unfortunately, “if you want to speed it up, use a NIF” is a statement that does not seems to hold much truth for situations such as these, where you want to implement a datatype on which individual operations are expected to run in the order of microseconds
. I’ve benchmarked the Rust-based implementation against the existing ErlangArray and MapArray ones, and it turns out that the overhead of the NIF-calls significantly overshadows the otherwise highly performant techniques used in this implementation. (Benchmark graphs can can be found in the README).
So, this was a worthwhile experiment, but it probably makes little sense to use the NIF-based array in production circumstances over the other ones. Maybe there is something I have missed in the implementation, and it still might serve as a nice example of ‘how to implement a NIF-based datastructure’, but it is not the “super efficient” solution that I’d hoped it would be.
So, back to the drawing board it is! Time to look for (or build) one of the more modern array-implementations in plain Erlang or Elixir code.
Qqwy
I have some better news: Recently I was introduced to the Aja library, which implements multiple of data structures focused on performance. Its
A.Vectoris a persistent immutable array implementation, written fully in Elixir.I wrapped
), and benchmarked them against the other implementations.
A.Vectorwith theArrays.Protocol(which was a breeze as Aja already exposes a very rich APIThe results are amazing!
Aja’s implementation is fast. As can be seen from below benchmarking graphs,
A.Vectorbeats the other alternative datatypes outright in most common tasks:A.Vectoralso really shines: It is roughly 5x(!) faster than ErlangArray and MapArray, regardless of collection size. It is only outclassed by plain lists (which are again ~5x faster for concatenation, but much (i.e. asymptotically) slower for all of the other three operations).You can find ArraysAja on GitHub in a separate library, of which
v0.1.0has been published to Hex.PM.