Erlang Records vs. Elixir Structs

I use Erlang and Elixir regularly, and I often get hung up on the differences between Erlang records and Elixir structs. Both serve the same purpose most of the time but are implemented differently.

In this blog post I will document the differences between these two constructs.

Read in full here:

This thread was posted by one of our members via one of our news source trackers.

2 Likes

Corresponding tweet for this thread:

Share link for this tweet.

1 Like

Similarities

I think it would be more accurate to say something like:

  • Erlang Record: Internally positionally static, static-time name indexable, Sized (size must be known at compile-time). These are most like the usual Prod Type with names, like a struct in OCaml.
  • Elixir Structs: Internally a hashmap, runtime name indexable with some static checks that can be skipped, Unsized. These are like row-typed polymorphism, more flexible but less efficient, like an object in OCaml.

Evaluation of Field Default

Both erlang records and elixir structs are either runtime or compile-time evaluated depending on their contexts, and the contexts are the same between them, so there’s no difference here.

Evaluation of Field Default Expression

Ah, they meant where the default expression is ‘executed’, and elixir means that it executes at compile-time then just copies the value around, yeah this is a very annoying issue at times since you can’t represent all types in the AST. Even making a simple elixir struct of functions is impossible as default values, instead you have to make a ‘constructor’, which is very easy to forget to use as there is no runtime check for this, so I try to be in the habit of always making a new function on the module and calling it to make the struct instead of making it directly. Erlang records do not have such a limitation (and you can have them do the same as the elixir style as well with a constructor for them too, I like constructors for such things regardless).

I copied and modified a benchmark script posted by OvermindDL1 on the Elixir Forum.

Oh hey! Wasn’t expecting that, lol. I’m curious of their results after the BEAM’s JIT has had major improvements lately!

Erlang 23.2.4

Aww, they didn’t test with the new JIT… so I’m going to! Results with their code (and here
s
a direct link to their results on their system for comparison):

Results
Benchmarking puts...
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record1...
Benchmarking Struct1...
Benchmarking Struct1-opt...

Name                  ips        average  deviation         median         99th %
Record1          109.81 M        9.11 ns ±82619.96%           0 ns         120 ns
Struct1           36.93 M       27.08 ns ±99103.89%           0 ns          80 ns
Struct1-opt       33.43 M       29.91 ns ±98042.27%           0 ns         100 ns

Comparison: 
Record1          109.81 M
Struct1           36.93 M - 2.97x slower +17.97 ns
Struct1-opt       33.43 M - 3.28x slower +20.81 ns
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record9-first...
Benchmarking Struct9-first...
Benchmarking Struct9-first-opt...

Name                        ips        average  deviation         median         99th %
Struct9-first-opt       26.31 M       38.00 ns ±77975.37%           0 ns         200 ns
Struct9-first           24.70 M       40.49 ns ±93988.44%           0 ns         200 ns
Record9-first           16.71 M       59.85 ns ±74623.83%           0 ns         180 ns

Comparison: 
Struct9-first-opt       26.31 M
Struct9-first           24.70 M - 1.07x slower +2.49 ns
Record9-first           16.71 M - 1.57x slower +21.85 ns
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record9-last...
Benchmarking Struct9-last...
Benchmarking Struct9-last-opt...

Name                       ips        average  deviation         median         99th %
Struct9-last-opt       31.41 M       31.84 ns ±82559.89%           0 ns         180 ns
Struct9-last           27.91 M       35.83 ns ±83978.40%           0 ns         190 ns
Record9-last           15.27 M       65.51 ns ±73057.59%           0 ns         189 ns

Comparison: 
Struct9-last-opt       31.41 M
Struct9-last           27.91 M - 1.13x slower +3.99 ns
Record9-last           15.27 M - 2.06x slower +33.67 ns
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record26-first...
Benchmarking Struct26-first...
Benchmarking Struct26-first-opt...

Name                         ips        average  deviation         median         99th %
Record26-first            7.62 M      131.26 ns ±33718.28%          90 ns         360 ns
Struct26-first            7.19 M      139.17 ns ±26895.41%          90 ns         350 ns
Struct26-first-opt        7.17 M      139.47 ns ±26698.41%          60 ns         339 ns

Comparison: 
Record26-first            7.62 M
Struct26-first            7.19 M - 1.06x slower +7.91 ns
Struct26-first-opt        7.17 M - 1.06x slower +8.21 ns
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record26-last...
Benchmarking Struct26-last...
Benchmarking Struct26-last-opt...

Name                        ips        average  deviation         median         99th %
Record26-last            7.48 M      133.73 ns ±34285.58%         100 ns         360 ns
Struct26-last            6.23 M      160.53 ns ±23863.56%         110 ns         430 ns
Struct26-last-opt        5.54 M      180.52 ns ±23235.03%         120 ns         430 ns

Comparison: 
Record26-last            7.48 M
Struct26-last            6.23 M - 1.20x slower +26.80 ns
Struct26-last-opt        5.54 M - 1.35x slower +46.79 ns
Benchmarking lookups...
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record1...
Benchmarking Struct1...

Name              ips        average  deviation         median         99th %
Record1        2.41 B        0.42 ns  ±8665.64%           0 ns           0 ns
Struct1        1.91 B        0.52 ns ±14940.70%           0 ns           0 ns

Comparison: 
Record1        2.41 B
Struct1        1.91 B - 1.26x slower +0.108 ns
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record9-first...
Benchmarking Struct9-first...

Name                    ips        average  deviation         median         99th %
Record9-first      170.73 M        5.86 ns ±60550.59%           0 ns          40 ns
Struct9-first       47.10 M       21.23 ns   ±483.41%          20 ns          80 ns

Comparison: 
Record9-first      170.73 M
Struct9-first       47.10 M - 3.62x slower +15.37 ns
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record9-last...
Benchmarking Struct9-last...

Name                   ips        average  deviation         median         99th %
Record9-last      401.04 M        2.49 ns±158030.56%           0 ns          10 ns
Struct9-last      257.61 M        3.88 ns  ±1737.50%           0 ns          50 ns

Comparison: 
Record9-last      401.04 M
Struct9-last      257.61 M - 1.56x slower +1.39 ns
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record26-first...
Benchmarking Struct26-first...

Name                     ips        average  deviation         median         99th %
Record26-first      334.95 M        2.99 ns  ±1685.45%           0 ns         110 ns
Struct26-first      301.88 M        3.31 ns  ±1207.72%           0 ns          50 ns

Comparison: 
Record26-first      334.95 M
Struct26-first      301.88 M - 1.11x slower +0.33 ns
Operating System: Linux
CPU Information: AMD Ryzen 7 1700 Eight-Core Processor
Number of Available Cores: 16
Available memory: 15.62 GB
Elixir 1.12.2
Erlang 24.0.3

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

Benchmarking Record26-last...
Benchmarking Struct26-last...

Name                    ips        average  deviation         median         99th %
Struct26-last      754.44 M        1.33 ns  ±3362.61%           0 ns           0 ns
Record26-last      589.61 M        1.70 ns±135814.43%           0 ns           0 ns

Comparison: 
Struct26-last      754.44 M
Record26-last      589.61 M - 1.28x slower +0.37 ns

The JIT doesn’t seem to make a huge difference, I’d figure data access would be pretty easy to optimize direct calls to… Some results are definitely HUGELY faster, that’s probably due to the JIT, but what’s up with the rest?

A first look at the JIT - Erlang/OTP

Oh wait, the JIT doesn’t do any optimizations at all, it just compiles each bytecode instruction individually into the corresponding machine code, so no optimizations are done, hrmm…

Conclusion

I’m still a fan of records over structs, and I still think Elixir structs should have just been erlang records underneath. Row-typed polymorphism is useful sure, but that should still have been its own separate type (maybe follow the OCaml way and call it object?).

1 Like