tim2CF

tim2CF

Elixir structs vs Erlang records

Hello!

Does anyone know why Elixir structures are implemented on top of Erlang maps, but not on top of Erlang records (tuples)? For me, Erlang records are looking more natural then maps for basement of Elixir structs, and besides in some cases it seems performance of records is better (for example in pattern matching), take a look:

iex(2)> defmodule Hello do
...(2)> defstruct [:foo]
...(2)> end
{:module, Hello,
 <<70, 79, 82, 49, 0, 0, 5, 144, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 182,
   0, 0, 0, 18, 12, 69, 108, 105, 120, 105, 114, 46, 72, 101, 108, 108, 111, 8,
   95, 95, 105, 110, 102, 111, 95, 95, 7, ...>>, %Hello{foo: nil}}
iex(3)> struct = %Hello{foo: 123}
%Hello{foo: 123}
iex(4)> fn -> 1..1000000 |> Enum.each(fn(_) -> %Hello{foo: foo} = struct; foo end) end |> :timer.tc  
{13719674, :ok} 
iex(5)> record = {Hello, 123}
{Hello, 123}
iex(6)> fn -> 1..1000000 |> Enum.each(fn(_) -> {Hello, foo} = record; foo end) end |> :timer.tc
{565886, :ok}
iex(7)>

Of course, you can say - “If you like Erlang records, just use it”, but Elixir infrastructure is very coupled with structs/maps - they are used everywhere (Ecto, Phoenix, Plug, Elixir standard libraries). So if I’m using Elixir, I don’t really have a choice - I have to use structs/maps

Most Liked

kokolegorille

kokolegorille

There is a good answer from Jose here about the choice of struct vs record.

11
Post #2
michalmuskala

michalmuskala

Please, please, don’t benchmark in the shell. The shell runs an interpreter and not the compiled code. The result will probably will be wildly different.

10
Post #3
OvermindDL1

OvermindDL1

Indeed, here it is in Benchee, I tried to prevent certain optimizations from happening by interning the test data in different modules than that which is accessed so things like the record macro’s don’t get optimized out and so forth (records were a lot faster than structs before I made that change).

Code struct_record_bench.exs:

defmodule AStruct1 do
  defstruct [a: 1]
  def news1(), do: %__MODULE__{}
end

defmodule AStruct9 do
  defstruct [a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9]
  def news9(), do: %__MODULE__{}
end

defmodule ARecords do
  import Record
  defrecord :aRecord1, [a: 1]
  defrecord :aRecord9, [a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9]
  def newr1(), do: aRecord1()
  def newr9(), do: aRecord9()
end

defmodule StructRecordBench do
  import AStruct1
  import AStruct9
  import ARecords

  def classifiers(), do: [:get, :put]

  def time_mult(_), do: 2

  def inputs(_) do
    nil
  end

  def actions(:get) do
    %{
      "Struct1" => fn -> news1().a end,
      "Struct9-first" => fn -> news9().a end,
      "Struct9-last" => fn -> news9().i end,
      "Record1" => fn -> aRecord1(newr1(), :a) end,
      "Record9-first" => fn -> aRecord9(newr9(), :a) end,
      "Record9-last" => fn -> aRecord9(newr9(), :i) end,
    }
  end

  def actions(:put) do
    %{
      "Struct1" => fn -> %{news1() | a: 42} end,
      "Struct1-opt" => fn -> %AStruct1{news1() | a: 42} end,
      "Struct9-first" => fn -> %{news9() | a: 42} end,
      "Struct9-first-opt" => fn -> %AStruct9{news9() | a: 42} end,
      "Struct9-last" => fn -> %{news9() | i: 42} end,
      "Struct9-last-opt" => fn -> %AStruct9{news9() | i: 42} end,
      "Record1" => fn -> aRecord1(newr1(), a: 42) end,
      "Record9-first" => fn -> aRecord9(newr9(), a: 42) end,
      "Record9-last" => fn -> aRecord9(newr9(), i: 42) end,
    }
  end
end

Results:

╰─➤  mix bench struct_record           

Benchmarking Classifier:  get
=============================

Operating System: Linux"
CPU Information: AMD Phenom(tm) II X6 1090T Processor
Number of Available Cores: 6
Available memory: 15.67 GB
Elixir 1.7.4
Erlang 21.1.1

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
parallel: 1
inputs: none specified
Estimated total run time: 36 s

Benchmarking Record1...
Benchmarking Record9-first...
Benchmarking Record9-last...
Benchmarking Struct1...
Benchmarking Struct9-first...
Benchmarking Struct9-last...

Name                    ips        average  deviation         median         99th %
Record9-last        23.29 M      0.0429 μs   ±107.83%      0.0400 μs      0.0700 μs
Record9-first       22.06 M      0.0453 μs     ±5.87%      0.0440 μs      0.0550 μs
Record1             21.61 M      0.0463 μs    ±10.12%      0.0450 μs      0.0640 μs
Struct9-first       18.14 M      0.0551 μs    ±19.80%      0.0560 μs      0.0660 μs
Struct1             17.93 M      0.0558 μs     ±8.76%      0.0560 μs      0.0700 μs
Struct9-last        17.46 M      0.0573 μs     ±6.31%      0.0560 μs      0.0720 μs

Comparison:  
Record9-last        23.29 M
Record9-first       22.06 M - 1.06x slower
Record1             21.61 M - 1.08x slower
Struct9-first       18.14 M - 1.28x slower
Struct1             17.93 M - 1.30x slower
Struct9-last        17.46 M - 1.33x slower

Memory usage statistics:

Name             Memory usage
Record9-last             72 B
Record9-first            72 B - 1.00x memory usage
Record1                  72 B - 1.00x memory usage
Struct9-first            72 B - 1.00x memory usage
Struct1                  72 B - 1.00x memory usage
Struct9-last             72 B - 1.00x memory usage

**All measurements for memory usage were the same**

Benchmarking Classifier:  put
=============================

Operating System: Linux"
CPU Information: AMD Phenom(tm) II X6 1090T Processor
Number of Available Cores: 6
Available memory: 15.67 GB
Elixir 1.7.4
Erlang 21.1.1

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
parallel: 1
inputs: none specified
Estimated total run time: 54 s

Benchmarking Record1...
Benchmarking Record9-first...
Benchmarking Record9-last...
Benchmarking Struct1...
Benchmarking Struct1-opt...
Benchmarking Struct9-first...
Benchmarking Struct9-first-opt...
Benchmarking Struct9-last...
Benchmarking Struct9-last-opt...

Name                        ips        average  deviation         median         99th %
Record1                 18.38 M      0.0544 μs   ±691.86%      0.0500 μs       0.120 μs
Record9-first           15.52 M      0.0644 μs   ±497.05%      0.0600 μs       0.140 μs
Record9-last            15.45 M      0.0647 μs   ±539.09%      0.0600 μs       0.150 μs
Struct1                 15.13 M      0.0661 μs   ±647.84%      0.0600 μs       0.130 μs
Struct1-opt             14.48 M      0.0691 μs   ±327.02%      0.0600 μs       0.110 μs
Struct9-first           14.31 M      0.0699 μs   ±309.43%      0.0600 μs       0.160 μs
Struct9-first-opt       12.73 M      0.0786 μs   ±328.39%      0.0700 μs       0.130 μs
Struct9-last            11.66 M      0.0857 μs   ±414.23%      0.0800 μs        0.21 μs
Struct9-last-opt        10.74 M      0.0931 μs   ±322.63%      0.0800 μs        0.21 μs

Comparison:  
Record1                 18.38 M
Record9-first           15.52 M - 1.18x slower
Record9-last            15.45 M - 1.19x slower
Struct1                 15.13 M - 1.21x slower
Struct1-opt             14.48 M - 1.27x slower
Struct9-first           14.31 M - 1.28x slower
Struct9-first-opt       12.73 M - 1.44x slower
Struct9-last            11.66 M - 1.58x slower
Struct9-last-opt        10.74 M - 1.71x slower

Memory usage statistics:

Name                 Memory usage
Record1                      96 B
Record9-first               160 B - 1.67x memory usage
Record9-last                160 B - 1.67x memory usage
Struct1                     112 B - 1.17x memory usage
Struct1-opt                 112 B - 1.17x memory usage
Struct9-first               176 B - 1.83x memory usage
Struct9-first-opt           176 B - 1.83x memory usage
Struct9-last                176 B - 1.83x memory usage
Struct9-last-opt            176 B - 1.83x memory usage

**All measurements for memory usage were the same**

So Records are faster in general (in all tested cases here actually) than Structs, but only marginally so, so much so that only the most performance sensitive code would really care, so in general most people shouldn’t care. :slight_smile:

I’m surprised that putting the struct type in the update syntax doesn’t make it faster actually as it could infer some existing structure, but I guess that all it would be doing is adding an extra runtime check or so (hence the module-optional variants are slower in the end).

EDIT: Personal Opinion time: Personally I’d prefer records were ubiquitous and used struct syntax (first class records in other words). Records in every language I’ve seen are statically sized, there is no point in them being maps, especially if they ‘own’ their module definition as structs do now then all the proper accessors for Access and extra data would all be accessible as they are for structs as well and as such by using those generated macro’s then you could generate getting/setting code that would be even more efficient than how structs work now. HOWEVER, Elixir is extremely poorly typed and doesn’t know what the type of a given thing would be, and Erlang works around that by requiring using the record name at all uses of a record variable, Elixir tries to be a little more succinct, and that succinctness is at odds with efficiency, and so the first-class syntax uses the slightly less efficient version in order for ease of use and relegates the more efficient version to a side set of macro’s since you require the names anyway. If Elixir had a decent typing system then you’d be able to have both efficiency and succinctness, but maybe that’s for an Elixir 2.0 or something. ^.^

EDIT: Hmm, a possible workaround for the first-class syntax access would be just dispatching based on the ‘module’ in the type-tag of the record, it would be a ‘remote call’ on the BEAM but might be good… I should test…

Where Next?

Popular in Discussions Top

andre1sk
A big advantage to Elixir is all the distributed goodness but for many applications running on multiple nodes having integrated Etcd, Zoo...
New
jeramyRR
This is an interesting article to read. Elixir’s performance, like usual, is excellent. However, it seems like the high CPU usage is co...
New
MarioFlach
Hello, I want to share a project I’ve been working on for a while: https://github.com/almightycouch/gitgud Background Some time ago I ...
New
WildYorkies
It seems that the more I read, the more I find Elixir users speaking about all the ways that Elixir is not good for x, y, and z use cases...
New
lucaong
Hello Elixir and Nerves community, I have been working for a while on an open-source embedded key-value database for Elixir, that I call...
230 13924 124
New
Qqwy
Looking at the stacks that existing large companies have used, WhatsApp internally uses Mnesia to store the messages, while Discord uses ...
New
praveenperera
How We Replaced React with Phoenix By: Thought Bot
New
eteeselink
Hi all, In the last days, two things happened: A blog post titled “They might never tell you it’s broken” made the rounds. It’s about ...
New
Crowdhailer
I’ve been hearing much about the new formatter and it’s something I have been keen to try. I find examples buy far the most illuminating...
248 19204 150
New
AstonJ
I’ve just started the Phoenix part of the utterly brilliant online course by @pragdave. On generating the Phoenix app he uses the --no-ec...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39297 209
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126479 1222
New

We're in Beta

About us Mission Statement