Suitability of Elixir to math-oriented apps

Or you may consider calling Julia from Elixir, a relatively new scientific language, close to C in performance.
Form the Julia website: (http://julialang.org/),
<Julia is a high-level, high-performance dynamic programming language for technical computing, with syntax that is familiar to users of other technical computing environments. It provides a sophisticated compiler, distributed parallel execution, numerical accuracy, and an extensive mathematical function library. Julia’s Base library, largely written in Julia itself, also integrates mature, best-of-breed open source C and Fortran libraries for linear algebra, random number generation, signal processing, and string processing…>

But not sure how it can be called from Elixir.
Regards.

1 Like

Julia: http://julialang.org/

“Julia is a high-level, high-performance dynamic programming language for technical computing, with syntax that is familiar to users of other technical computing environments. It provides a sophisticated compiler, distributed parallel execution, numerical accuracy, and an extensive mathematical function library. Julia’s Base library, largely written in Julia itself, also integrates mature, best-of-breed open source C and Fortran libraries for linear algebra, random number generation, signal processing, and string processing.”

i was recently talking with a trader friend working at one of the bigger banks. His work involves alot of computation-intensive modeling and simulations for options pricing, and he mentioned they have moved to GPU server farms. Pretty much they are using C/C++ with the NVIDIA CUDA runtime API, I am guessing for the reasons Jose Valim mentioned (ease of huge matrix computation, ability to use pointers on mutable data structures, etc).

Here is a brief paper describing the space, if you are interested:

3 Likes

Can someone confirm/deny possibility of calling Julia from Elixir?
I’ve dabbled with Julia awhile, it’s “Elixir-cool”. :slight_smile: Can’t say the same for C/C++ (and both Rust and Nim have a lot of library catch-up to do before they have C/C++'s math coverage).

Being able to call Julia would be a great benefit.
But is it possible yet?

You can integrate pieces of code written in other languages in a couple of ways. My first choice is always a port. I’ve blogged a bit about it here. Another options is to run in-process C code through NIF. See here for a good explanation. It looks like Julia can be embedded so both options should be possible.

4 Likes

There is also the scheduling issue with NIF’s. Any function you provide via a NIF needs to run in roughly a millisecond to avoid bolixing the standard scheduler. See the section around “Long running NIF’s” in

http://erlang.org/doc/man/erl_nif.html

This makes embedding potentially very long running math routines difficult. There are a couple ways around this, “dirty schedulers” is the most promising but that is still experimental.

Depending on the exact use case it may make the most sense to use Elixir as a scheduler and use the underlying file system as the data transport. (i.e. build an input file, monitor the math heavy program and read the results in when done.)

1 Like

Depending on the nature of the code you are using, you can handle scheduling issues without a dedicated scheduler.

You can see an example here of how you can replace a for loop with a recursive function, written in Elixir, which calls the bf_expand NIF. Each call to bf_expand is very short, so there are no scheduling issues.

This not always possible, though, and I’m going to be using dirty schedulers for another app I’m working on.

That’s why I always say that ports are my first choice :slight_smile:

2 Likes

I took a brief look at Julia, and from what I could tell it did not support or leverage multicore computers. How can it be “high performance” if it only runs on a single core? Or did I miss something?

Plenty of algorithms lack straight forward parallel implementations. State of the art for quite a lot of algorithms is a single threaded C or C++ implementation.

This isn’t even necessarily a problem, as in production it’s often easy to just run multiple jobs in parallel.

Yes, you did. Julia does not do concurrency (which is performing multiple separate tasks at the same time), but it does allow parallellism (which is performing the same or similar tasks, such as the same calculation on multiple pieces of data at the same time).

Elixir is great at the former, which makes it work consistently even when under high loads. Julia (and some other environments) have the latter, which allow it to have ‘high performance’ when doing number crunching.

I believe using the @parallel-keyword in combination with a loop is enough to let it divide work across your processors. Short explanation can be found here.

3 Likes

I heard that Elixir is not good at number crunching, but I didn’t expect it to be this bad! I tried to solve Problem #14 on Project Euler using both Ruby and Elixir. Ruby gave me the correct answer in 1.5s on my Macbook, while Elixir just letted me wait and wait and wait until I lost my patience and pressed Ctrl + C.

Here is the Ruby code:

class Collatz
  def initialize               
    @cache = {}             
  end

  def longest(upperbound)      
    1.upto(upperbound).map{|n| [n, sequence_length(n)]}.max_by{|_, len| len}
  end
  
  def sequence_length(n)       
    return 1 if n == 1         
    len = @cache[n]            
    if len
      len
    elsif n.odd?
      @cache[n] = sequence_length(3 * n + 1) + 1
    else
      @cache[n] = sequence_length(n / 2) + 1
    end
  end
end

require 'benchmark'

collatz = Collatz.new
puts Benchmark.measure {
  p collatz.longest(1_000_000)
}

And here is the Elixir code, which is not optimized to tail recursion (I’m still struggling how to do tail recursion with cache):

defmodule Collatz do           
                               
  use Agent
  require Integer

  def start_link do
    Agent.start_link(fn -> %{} end, name: __MODULE__)
  end
  
  def longest(upperbound) do   
    Stream.interval(1)         
    |> Stream.map(fn n -> {n + 1, sequence_length(n + 1)} end) 
    |> Stream.take(upperbound) 
    |> Enum.max_by(fn {_, len} -> len end)
  end

  def sequence_length(1), do: 1

  def sequence_length(n) do
    len = Agent.get(__MODULE__, &Map.get(&1, n))
    
    cond do
      len != nil -> 
        len
      Integer.is_odd(n) ->
        new_len = sequence_length(3 * n + 1) + 1
        Agent.update(__MODULE__, &Map.put(&1, n, new_len))
        new_len
      true ->
        new_len = sequence_length(div(n, 2)) + 1
        Agent.update(__MODULE__, &Map.put(&1, n, new_len))
        new_len
    end
  end
end

Collatz.start_link()

:timer.tc(fn ->
  Collatz.longest(1_000_000)
end)
1 Like

The Elixir code you’ve got isn’t a particularly suitable adaptation, which isn’t particularly that uncommon when folks are getting going. By way of comparison https://github.com/sorentwo/euler/blob/master/lib/euler_014.exs runs in ~4.7 seconds on my computer and it doesn’t even have any caching.

5 Likes

That’s interesting. After I removed caching, the performance gets much better (~3.4s). Then after I introduced tail recursion, the performance gets a little better (~3.2s), so non-tail recursion isn’t the cause of low performance. When I did the same thing in Ruby code (well, Ruby doesn’t optimize tail call so I didn’t do tail recursion in Ruby), the performance gets worse (~8.5s). So the performance of Elixir isn’t that bad. My fault. It seems that the worst part of my former Elixir code is the use of Stream (but why?).

But again, I’m still wondering how to do tail recursion with cache. I counted how many steps are saved with caching in Ruby, and it saved 130,265,812 steps, only 2,168,610 steps are actually calculated. When I added caching back to Elixir code, though this time it didn’t make me wait forever, the performance still got worse (~13s), but how can this be if it saved so many steps?

Instead of using an agent for cache, can’t you pass a map around?
*Edit, I’m on my phone right now, but if you don’t know what I mean I can show you

1 Like

I understand what you say. I just need some time to arrange the code if I need to passing and returning a map across all function calls. This is gonna be error-prone.

UPDATE

OK, I managed to do as what you said, but the performance (~3.7s) is still worse than no cache.

Here’s the new Elixir code:

defmodule Collatz do           
                               
  require Integer              

  def longest(upperbound) do   
    (1 .. upperbound)          
    |> Enum.reduce({0, 0, %{}}, fn(n, {m, max_len, cache}) ->
      case sequence_length(n, cache) do
        {len, new_cache} when len > max_len ->
          {n, len, new_cache}  
        {_len, new_cache} ->   
          {m, max_len, new_cache}         
      end
    end)
    |> elem(0)
  end

  def sequence_length(1, cache), do: {1, cache}

  def sequence_length(n, cache) do
    len = Map.get(cache, n)
    {new_len, new_cache} = cond do  
      len ->
        {len, cache}
      Integer.is_even(n) ->
        sequence_length(div(n, 2), cache)
      true ->
        sequence_length(3 * n + 1, cache)
    end
    {new_len + 1, Map.put(new_cache, n, new_len + 1)}
  end

end

Try using an ets table for caching.

1 Like

I’m skeptical that any data store external to the process will perform well. ets is great when you need a cache shared between processes, but within a single process a map or the process dictionary should be faster.

1 Like

Update: the ets code is using a bulk insert (a list of tuples) - giving it the apparent edge over the map code. Replacing individual Map.put/3 calls with a consolidated maps:merge/2 call improved matters for the map as a cache case.

For the process dictionary approach see:
Improving a slow solution for Longest Collatz Sequence

1 Like

Excellent question. When you use stream, you are typically trading CPU for memory. I.e. it is more computationally expensive but it allows you to work with large or even infinite datasets.

10 Likes