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
?).