Any tips or learning resources for writing highly performant Elixir code?

I’ve been writing Elixir and Phoenix professionally for the past year and have grown quite fond of them. I’ve read books like Elixir in Action, Concurrent Data Processing in Elixir, and Designing Elixir Systems with OTP.

I’m keenly interested in writing highly performant code in Elixir. Obviously, achieving performance levels like Go, C++, or Rust isn’t possible, and I understand that. However, I suspect there might still be some hidden tricks to make Elixir code more performant.

Could anyone point me towards any source material, books, talks, etc., that dive into the performance aspects of Elixir?

2 Likes

They are - just use NIFs :wink:

There are lots of possible improvements, for example:

  1. Depending on specific case i.e. you have 2 or more algorithms, benchmark them using benchee and use desired solution
  2. NIF and related language features not only can speed up code, but also allows to use many non-Elixir libraries.
  3. Stream, Flow and other modules instead of Enum - instead of storing whole enumerable in memory we can read and process them separately… Similarly we can process multiple list elements at the same time. There are lots of solutions depending on what you want to achieve
  4. Metaprogramming i.e. improve runtime code by for example generating a pattern-matching
  5. Try to limit piped Enum calls, so iterate one list as few times as possible, often [head | tail]-based recursive function or Enum.reduce/3 is useful in such cases.

Please remember that there is no #1 optimisation. Sometimes you optimise resources usage and sometimes you use all available resources to speed up the work. Also it’s worth to mention that raw speed it not always welcome if you have to write very strict code. Often writing more generic code without every possible optimisation may save you huge amount of time in case you would be rewriting or enhancing it by supporting more structs and so on … Again, there is no a single good solution here as it’s very case-specific topic.

4 Likes

The most performance I managed to eke out of Elixir was:

  1. When I parallelized the algorithm. The BEAM VM just absolutely excels at parallel programming.
  2. When I made sure to not copy and pass a lot of data around i.e. if you need to periodically access stuff that’s several kilobytes it’s probably best to put it in ETS and just pass names / references to your workers – and not the data itself. That’s also quite true for any programming language btw; you can make an otherwise quick Rust program crawl down to JS / Golang level if you just constantly copy / clone data. (BTW it really must be said that this very strongly depends on the data, the algorithm, the amount of workers etc.; I also had success with just directly passing the data to workers and was confused as to why using ETS didn’t net me a performance win… until I realized that pulling data out of ETS copies them as well – so it’s all copying in the end but many other parameters in the equation can tilt the result one way or the other. Just measure.)

There are many more that could be inferred by production experience but these were my top 2 every time. I’d say you’ll have more success if you just try your hand at something and if you are not satisfied with the results, come back to the forum and we’ll give you guidance on per-case basis.

5 Likes

Without much more information about what you want your code to do, I can only offer more general advice to think about when writing code, as a supplement to Eiji’s great advice.

Do less.

It should not be surprising that code that does less usually finishes faster than code that does more. Applying this advice could look like:

  • Refactoring traversals of collections to traverse less, ideally once. (Echoed in Eiji’s post).
  • Performing fewer deep structure updates. While updates to immutable data are efficient from a VM perspective, they’re not free. Certain updates to data structures are much cheaper than others, and you should try and use those whenever possible. For example, building iolists can be much faster than repeatedly constructing binaries.

Using :ets, :atomics, and more.

Erlang’s standard library contains modules that can be a great help when writing code that needs high performance. :atomics and :counters are great for working with collections of integers that must be updated atomically. :persistent_term is very useful for accessing read-only data from many processes.

ETS is a more general-purpose tool, and learning to wield it well can dramatically improve performance in many situations.

Read the Erlang Efficiency Guide

https://www.erlang.org/doc/system/efficiency_guide.html

Reading, and more importantly, understanding the advice written in the Erlang Efficiency Guide should take you pretty far along your journey to writing high-performance code that runs on the BEAM. The guide does a great job of explaining why certain code runs more slowly, and communicates useful insight to the BEAM VM that you can keep in your mind when you code.

Know when to stop

While it is very satisfying to write code that runs very quickly, certain optimizations and refactorings done in the name of performance can have a strong negative impact on the readability, testability, and maintainability of your code. To quote the late, great Joe Armstrong:

Make it work, then make it beautiful, then if you really, really have to, make it fast. 90 percent of the time, if you make it beautiful, it will already be fast. So really, just make it beautiful!
– Joe Armstrong, Erlang & OTP in Action

9 Likes

knowing went to stop is key. Making that background job run in 10s instead of 12s when it only runs 1 time a day does not create any actual benefit

4 Likes

Nine times out of ten, in my career, the biggest wins have from finding better ways to model the data (MapSet, :ordset, and :digraph have helped with this in Elixir code) and figuring out how to skip steps with better algorithms. My new Livebook series, How to Train Your Scrappy Programmer, covers a lot of those techniques (using several practices mentioned in this thread and others). I would say that the two Livebooks most heavily covering this topic are Borrowing a Cup of Algorithms and Charting Our Course. There’s also some of this thinking in the free download: Data and the Code That Loves It.

6 Likes

For those who are more experienced in writing performant code, I would love to see someone in action taking some piece of slow code or a library and making it run fast. I would pay good money for a series on just that!

I have heard of the tools like fprof and benchee. I have also heard advice not to trust benchmarks. Or the advice to use a profiler to identify the bottlenecks before optimizing anything.

But it still feels like a bit of a dark art to me. How does one analyze the bottleneck and test out improvements? What mental models in Elixir do we need to know X is probably faster than Y?

Maybe I’m shouting into the void here, but if you are experienced at optimizing code, next time you have to optimize something that’s not proprietary, please hit record and publish it :slight_smile:

My How to Train Your Scrappy Programmer series is a tour through the solutions to five days of Advent of Code problems from 2023. Since Advent of Code often involves the optimization of search problems, my series covers a lot of exactly what you asked for. It generally begins with inefficient solutions, examines why they are slow, and adds on optimizations to demonstrate improvements.

I hope you’ll consider checking it out and, if you do, please let me know if it did deliver what you are searching for.

2 Likes

First, I would go read about profiling in Elixir, for example Erlang in Anger. Because first step to optimisation is knowing where hot spots are.

2 Likes