Writing Fast Elixir
Genetic Algorithms in Elixir — by Sean Moriarity (83 / 101)
👈 Benchmarking and Profiling Genetic Algorithms | TOC | Improving Performance with Parallelization 👉
One of the first rules of thumb for optimizing code states: “you can’t optimize a bad algorithm.” The most significant performance gains in your code will often come from better algorithms. Efficient algorithms can make up for limitations in computing power.
A comprehensive introduction to data structure and algorithm design is outside of the scope of this book. If you want to learn about writing efficient algorithms and improving your code’s performance, check out A Common-Sense Guide to Data Structures and Algorithms [Wen20].
Instead, this section will cover some basic tips to increase the performance of your Elixir code — this is simply an overview and is not meant to be a comprehensive guide for writing fast Elixir. You can reference Fast Elixir[18] for a more in-depth look at the performance of different functions.
Reduce or Map?
A basic optimization you can make when you don’t need to preserve the order of your lists is to replace Enum.map/2 with Enum.reduce/3. The reason this works is because all of Elixir’s Enum functions are implemented using a reduce function. For example, in crossover, you could just as easily use Enum.map/2 to apply crossover to every pair of parents in the population, like this:
def crossover(population, opts \\ []) do
crossover_fn = Keyword.get(opts,
:crossover_type,
&Toolbox.Crossover.single_point/2)
population
|> Enum.map(fn {p1, p2} -> apply(crossover_fn, [p1, p2]) end)
endThis function is much more concise than your original implementation:
def crossover(population, opts \\ []) do
crossover_fn =
opts
|> Keyword.get(:crossover_type, &Toolbox.Crossover.single_point/2)
population
|> Enum.reduce([],
fn {p1, p2}, acc ->
{c1, c2} = apply(crossover_fn, [p1, p2])
[c1, c2 | acc]
end
)
endHowever, it’s also slightly less performant. Note that Enum.reduce/3 will reverse the order of your list. If you need to preserve order, use Enum.map/2.
Take and Drop
When using Enum.take/2 and Enum.drop/2, you have the option to use negative numbers to indicate “taking” or “dropping” from the back of the list. Doing so will result in a significant performance drop.
The reason lies in how these two functions are implemented in the Elixir standard library. If you find yourself using Enum.take/2 with a negative number, like this:
x = Enum.take(some_list, -50)You can replace it with Enum.drop/2, like this:
x = Enum.drop(some_list, 50)Performing this “optimization” on my machine resulted in an almost 2x increase in performance. It’s also more correct to use drop in this instance.
Lazy Evaluation
Elixir offers the Stream module which allows you to use lazy evaluation over eager evaluation. Lazy evaluation ensures that you only execute functions when you need to. It eliminates unnecessary calculations. In general, the Stream module is slightly less performant than Enum for the same tasks; however, Stream offers some benefits that can increase performance.
For example, when creating new populations, you can use Stream.repeatedly/2 in initialize, like this:
def initialize(genotype, opts \\ []) do
Stream.repeatedly(genotype)
endYou can then pass the Stream from function to function until you absolutely need the entire population. This saves some memory up until you need to produce the entire population for selection, crossover, and mutation. In general, you might not see performance increases from this approach; however, you can benefit from using streams when you need to cut out unnecessary calculations.
👈 Benchmarking and Profiling Genetic Algorithms | TOC | Improving Performance with Parallelization 👉
Genetic Algorithms in Elixir by Sean Moriarity can be purchased in other book formats directly from the Pragmatic Programmers. If you notice a code error or formatting mistake, please let us know here so that we can fix it.







