Improving Performance with NIFs
Genetic Algorithms in Elixir — by Sean Moriarity (85 / 101)
👈 Improving Performance with Parallelization | TOC | What You Learned 👉
Native Implemented Functions (NIFs) are a great way to inject speed into your Elixir/Erlang programs. NIFs are programs implemented in compiled languages like Rust or C/C++ that are then linked and loaded into your Elixir module at runtime.
NIFs were designed to be a simpler and more efficient way of interfacing with native code than ports. Ports interact with external programs and offer a mechanism of implementing program features in a different language.
NIFs are often used in places where Elixir/Erlang alone isn’t enough to efficiently get the job done. For example, the Matrex[20] library uses NIFs to perform fast matrix operations because Elixir/Erlang isn’t optimized to perform these operations alone.
One thing to consider with NIFs is that they have the potential to crash your program. NIFs take the fault-tolerance out of applications because the code loaded is from an external program. Additionally, NIFs have a strange way of interacting with Erlang’s scheduler. The scheduler expects NIFs to return in a certain amount of time; however, if they don’t, it can dramatically impact the performance of your programs.
You can use NIFs to boost performance on your crossover and mutation functions or to augment Elixir implementations of slower functions. For example, random number generation is a notoriously expensive task. You can augment random number generation with a simple and fast C/C++ implementation of a more basic RNG like an XOR-Shift RNG. This repository[21] implements a number of XOR-shift RNGs in C.
To get a better idea of how to implement NIFs, you’ll implement a basic RNG NIF in C. Start by creating a new file genetic.c in a new directory src. In the file, add the following:
#include <erl_nif.h>
#include <inttypes.h>
#include <stdint.h>
static uint32_t x = 123456789, y = 362436069, z = 521288629;
static ERL_NIF_TERM xor96(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
uint32_t t = (x^(x<<10));
x = y;
y = z;
z = (z^(z>>26))^(t^(t>>5));
return enif_make_int(env, z);
}
static ErlNifFunc nif_funcs[] =
{
{"xor96", 0, xor96}
};
ERL_NIF_INIT(Elixir.Genetic, nif_funcs, NULL, NULL, NULL, NULL);This code defines an XOR96 random number generator and returns it to the Erlang environment using the Erlang NIF C interfaces. Most of the code you see defined here is boilerplate code necessary to create NIFs in C.
Next, you need to define a new Mix compiler in mix.exs, like this:
defmodule Mix.Tasks.Compile.Genetic do
use Mix.Task.Compiler
def run(_args) do
{result, _errcode} =
System.cmd(
"gcc",
["-fpic", "-shared", "-o", "genetic.so", "src/genetic.c"],
stderr_to_stdout: true
)
IO.puts(result)
end
endThis code uses the Mix.Compiler API to create a new Mix compiler that will compile your NIFs to a shared-object library using GCC. If you’re using Windows, you’ll have to either use MingW, Windows Subsystem for Linux (WSL), or integrate this workflow with Visual Studio. MingW is probably the most straightforward option. It’s a port of the C compiler, GCC, to Windows.
Next, you add your compiler to your projects :compilers like this:
defmodule Genetic.Mixfile do
use Mix.Project
def project do
[
app: :genetic,
...
compilers: [:genetic] ++ Mix.compilers,
]
end
...
endNow, whenever you compile your algorithms, Mix will run your custom compiler and compile your NIFs for you. To call xor96 from your code, add the following to the top of genetic.ex:
defmodule Genetic do
@on_load :load_nif
def load_nif do
:erlang.load_nif('./genetic', 0)
end
def xor96, do: raise "NIF xor96/0 not implemented."
...
endThis code defines a function to run once the module is loaded. The function load_nif/0 uses Erlang’s load_nif/2 function to load your shared object library at runtime. You also define a fallback implementation of xor96 to run if Elixir can’t find your C implementation.
xor96/0 will generate integers. You can use it to generate integers between 0 and some number, like this:
iex(0)> rem(xor96(), 100)
42In an example function like single_point_crossover/2, it would look like this:
def single_point_crossover(p1, p2) do
cx_point = rem(Genetic.xor96(), p1.size)
{p1_head, p1_tail} = Enum.split(p1.genes, cx_point)
{p2_head, p2_tail} = Enum.split(p2.genes, cx_point)
{c1, c2} = {p1_head ++ p2_tail, p2_head ++ p1_tail}
{%Chromosome{genes: c1, size: length(c1)},
%Chromosome{genes: c2, size: length(c2)}}
endOne issue with this RNG is that it starts from the same state every time your application is run. So it will produce the same order of random numbers every time you run your genetic algorithm. You can find other, better ways to take advantage of NIFs. For example, you could implement entire crossover or mutation functions using NIFs and use your current implementations as fallbacks in case the NIFs don’t load for some reason.
👈 Improving Performance with Parallelization | TOC | What You Learned 👉
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.

