Understanding Elixir/Phoenix performance

Thank you!

The first version is the one posted in message # 10, but after applying suggestions from @garazdawi and @josevalim the final version is in message #16 (about three times faster).

sorry but where has you readed that go doesnt outperform elixir???..almost any (except for a benchmark posted in github probably) shows that go has much better performance.

It’s not a criticism to elixir, elixir is much more robust and the ending app would be much more stable with less effort than golang…but in performance terms I think that go must be superior for several orders of magnitude

I should probably have worded it clearer.

As I wrote Go has both excellent raw speed and excellent concurrency. However I expect - just guessing - 1 or two order of magnitude, not “several” in tasks mainly requiring raw-speed.

But I got the idea that in real world massive concurrent web sites (note that I was asking about Elixir/Phoenix so a particular set of applications) the difference is not that big. In other words I don’t think WhatsApp would be orders of magnitude faster if written in Go instead of Erlang!

So my doubt (now cleared): what makes Elixir/Phoenix performing so much better than I expected?

2 Likes

The main advantage of Go in a situation like that is not necessarily speed (it’s almost entirely IO bound after all), but rather memory consumption of Go would be far less. However, Go is substantially less ‘safe’ than BEAM programs in terms of staying up in the cases of failure, so it’s a trade-off between maybe needing more servers and being able to fairly easily (comparatively) scale BEAM programs out or have a faster but with potentially more downtime Go program.

3 Likes

Came across this after having the same thoughts - I’ve just started learning Elixir and really like it but there is a small niggle that some simple tasks take an order of magnitude more time to run in Elixir/Erlang than in other languages. For example, just calculating the 20th Fibonacci number (recursive implementation) in Elixir takes around 300 microseconds on my laptop but in Golang it takes around 35 microseconds - anyone know how there can be such a difference in performance?

Elixir

defmodule Fib do
  def of(0), do: 0
  def of(1), do: 1
  def of(n), do: Fib.of(n-1) + Fib.of(n-2)
end

before = :os.system_time(:microsecond)
res = Fib.of(20)
now = :os.system_time(:microsecond)

IO.puts "took: #{now-before} microseconds"

Golang

func main() {
	now := time.Now()
	res := fib(20)
	took := time.Since(now)

	log.Printf("res: %d took: %s", res, took)
}

func fib(x int) int {
	if x == 0 {
		return 0
	}

	if x == 1 {
		return 1
	}

	return fib(x-1) + fib(x-2)
}

This has been addressed in more detail in other threads, but I think a fair TL;DR is:

The BEAM (and by extension, Elixir which runs on the BEAM) values the progress of the whole system over the optimal progress of some tiny part of that system.

This philosophy is at the core of all of the major Elixir trade offs I think. The things that make a single process slower when processing CPU bound tasks are exactly the same things that ensure that a CPU bound task can’t lock up the whole system. It’s the things that allow you to introspect and trace in realtime the execution of the system to figure out where the slow parts are on your running production server. Its the things that ensure that if part of the system fails, it won’t leave other parts with invalid memory states. These trade offs are great if you’re running an IO oriented server that coordinates many things or manages many data streams. These trade offs are bad if you want to write say a video codec or a 3d renderer. That’s OK, it’s one of the things I like about Elixir: It is up front and honest about what it’s good at and what it isn’t. It makes clear trade offs.

Addendum: A slightly more realistic CPU bound performance comparison might be JSON parsing, you can find an example here: Crude JSON parsing benchmarks for Elixir, Ruby, Golang. In this case at least, Elixir actually out performs Go.

8 Likes

for elixir you’ll want to use Tail recursion… https://www.stridenyc.com/blog/tail-call-optimization-with-fibonacci-in-elixir

for golang you’ll actually want to use the iterative one (afaik): https://medium.com/@felipedutratine/iterative-vs-recursive-vs-tail-recursive-in-golang-c196ca5fd489

and now you end up comparing different algos - due to different language features and performance characterics etc. etc.

also see this post/video (probably somewhat outdated by now) Performance - Best practices? on how Elixir/Erlang trades throughput for predictable latency - and Go does the opposite…

3 Likes

Taking this specific example:

  • Go’s int is a fixed size, 32 or 64 bit. Erlang/Elixir uses bignums, they can take huge values without overflow, but are slower.
  • Fib.of(n) is what’s called an external or fully qualified call in Erlang, as opposed to a local call (of(n)). It will always use the latest version of the module that’s loaded. The code reload mechanism adds flexibility but makes every function call a bit slower. It also means the compiler can’t assume that Fib.of will always return an int, so the code has to check the type every single time before doing the + - whereas the Go compiler can skip those checks. (The compiler may be able to optimize a bit better if you use defp to define the recursive function and use a wrapper e.g. def fib to call it externally)

You’ll find many similar tradeoffs in Erlang where it aims for reliability or observability rather than performance.

Sometimes it’s also just slow because it’s slow. It doesn’t get as much investment as other platforms (Java in particular), so potential improvements like the JIT compiler take a long time. The compiler is getting a lot of interesting improvements in the next release (23), in type analysis and optimization for instance.

4 Likes

Thanks for taking the time to respond - yeah I did a quick json comparison where I encoded/decoded an example map from the jason github page 100,000 times in Elixir and Go and Elixir was faster so interesting suggestion there. Weird how there was so much of a difference in performance with the Fibonacci calculation but not with some other tasks.

As you say its the use case that dictates the performance vs reliability trade off decision and it seems that Elixir is slow in some cases but not others so I’ll be interested to monitor this on various examples through my Elixir journey.

1 Like

I’m surprised Elixir was faster. To me that suggests problems with Go’s JSON implementation. Because generally on CPU calculations the BEAM is substantially slower, but it is blazing fast on moving IO around, and superb at doing lots of that in parallel.

2 Likes

In my experience, most of such synthetic challenges are significantly skewed due to a suboptimal algorithm. In this example, we can significantly improve the running time by changiing the algorithm. Here’s a demo on 40th fibonacci:

defmodule NaiveFib do
  def of(0), do: 0
  def of(1), do: 1
  def of(n), do: of(n - 1) + of(n - 2)
end

{time, res} = :timer.tc(fn -> NaiveFib.of(40) end)
IO.puts("Naive fib returned #{res} in #{time} microseconds")

defmodule BetterFib do
  def of(0), do: 0
  def of(1), do: 1
  def of(n), do: of(0, 1, 2, n)

  defp of(x, y, n, n), do: x + y
  defp of(x, y, m, n), do: of(y, x + y, m + 1, n)
end

{time, res} = :timer.tc(fn -> BetterFib.of(40) end)

IO.puts("Better fib returned #{res} in #{time} microseconds")

Output:

Naive fib returned 102334155 in 2776981 microseconds
Better fib returned 102334155 in 0 microseconds

As we can see, the algorithmical optimization leads to an almost instantaneous computation. Of course, the equivalent go version would still be faster, but at this speed the difference doesn’t matter. So more generally, an algorithmical optimization can do wonders for performance. If that still doesn’t cut it, it’s perhaps time to consider a faster language. If that is the case, I’d probably go for C or Rust instead of go. But in my experience, more often than not, the challenge can be solved within a reasonable time using a proper algorithmical/technical combo :slight_smile:

21 Likes

That’s a really good takeaway and it has been my feeling (backed with measured results) as well.

Not sure why people even come to dynamic languages expecting miracles of performance. Maybe they don’t realise that a lot of stuff in Python, JS and Ruby uses C libraries?

1 Like
  1. I’m pretty sure that’s the case.

  2. Elixir is easily 10x faster than Ruby at the CPU-specific parts (talking to database, parsing results, formatting output), so naturally Ruby folk see it as “blazing fast”. What they don’t realize is that C (Java, Go, Rust, etc) can be 100x-1,000x faster at those things than Ruby, thus also much faster than Elixir :wink:

1 Like

Which Elixir version is that? It returns fine on 1.9.4

NVM… I’m an idiot. I did not see the post date.

I wouldn’t even say that is why people are always arguing over performance.

Real life example, we deployed an App last week, a rewrite of a Ruby (Sinatra) app, in Phoenix w/o assets etc (pure JSON API). We noticed that response times were about 150ms slower than with the Sinatra app.
After further investigation, it turns out the apps compare very different on localhost. Phoenix would be much faster on localhost, with differences as big as Phoenix returning in 3ms and Sinatra in 80ms, but in other instances Sinatra being faster. Both in development mode, mind you.
My best guess would be GC, DB connections being reestablished, maybe some caching etc kicking in at different times.

Further investigation turned out that the delay on the production server was a pretty consistent 80ms times X. Guess what, production app was slower because of the distance between app server to database server, meaning that a database query had to go from Singapore to the US based DB server, then back, with every database query. The old server was in the US, so there was no reasonable speed comparison.

Network connection is real, and in my experience usually a huge factor in how fast/snappy a web app can be. 120ms from Japan to US West coast is considered fast, do that 5 times, and there goes half a second. It’s usually a much bigger factor than the (lets say) 100ms Elixir could give you over Ruby.

Even if Elixir was slower than Ruby, it would have an easier time spawning new processes over Ruby (especially Rails) instantiating new instances, but you would not notice until you hit a certain concurrency level.

Languages have their weaknesses. PHP is the grumpy Grandpa shouting at clouds. C is the fastest way into the next wall - like a racing car with no breaks. Ruby is the nicest way to shoot yourself in the knee in a shiny way. Elixir is the worst way of improving speed coming from Ruby - “I built this boat exactly as I did my car, why does it not go fast!”. Go is Google.

Well, my numbers come from comparing carefully-designed comparables, with the db co-located on the machine and accessed via domain socket :wink:

1 Like

There are definitely cases where Ruby will outperform Elixir. For example, Ruby’s compact is implemented in C, while in Elixir we have to iterate a linked list in Elixir code. Ruby will inevitably work faster here (though Elixir will have some other benefits, such as non-blocking code).

Another thing going for Ruby’s performance is mutability. Yes, mutability is usually bad, and even though I’ve spent more than half of my programming life happily working with mutable data, I agree that immutable is a saner default in most cases. Regardless, there are occasional scenarios where mutability will do wonders for performance.

So I wouldn’t generalize that either is faster, and TBH it usually doesn’t matter. As I like to say (and I already said it here), if proper algorithmical and technical optimizations are done, the differences between any two languages frequently become irrelevant in practice. At least that’s my own limited experience. If the performance is still not good enough and we want to squeeze out every nanosecond, than neither Ruby nor Elixir are likely good choices, and hence the comparisons of their performances is IMO usually not very interesting or useful :slight_smile:

3 Likes

I don’t see this as being relevant at all for any comparison between any two languages and/or frameworks? Sorry if I misunderstood, you were probably making a bigger point.

Because, it turns out, Elixir is not magic per se. It requires craftsmanship and playing to its strengths (which is actually very easy and I get frustrated when people treat Elixir as if it’s a mind-reader or Skynet-level general AI that will write their programs for them). But then again, needing craftsmanship skills applies to all technologies. Golang is derided in many programming circles yet the professional Golang devs can make wonders with it, both in terms of reliability and speed.

(EDIT: Still though, just by writing idiomatic Elixir – which isn’t hard and is times easier compared to Go or Rust – the average response time of my Phoenix apps is anywhere between 5x to 120x quicker than similar Rails or PHP or Python apps.)

Yep, agreed! Many of us are acutely aware of the strengths and weaknesses of Elixir (or any other tech we use, for that matter). I have factually and measurably observed that in 99% of my projects the app is waiting on a network or disk. However, Rails’ ActiveRecord wastes a lot of time serializing data from/to the DB while Ecto doesn’t. Additionally, outside of Rails and Laravel I didn’t hear about many frameworks that actually help you get productive quickly (maybe Rocket in Rust, still haven’t tried it but looks promising).

So for most webapps work Elixir clearly wins – in my eyes at least, because it can juggle a metric ton of I/O and CPU work without lagging or putting OS process pools in your face (as most of Ruby’s production servers force you to). Hosting costs are smaller as well, and things break much, much less frequently.

As usual, it’s about tradeoffs. If somebody finds themselves angry at the speed of a dynamic language then, well, obviously it’s time to learn C, C++, Go or Rust.

1 Like

I meant to mention that in my first post here, but spaced and forgot it. ActiveRecord is incredibly heavyweight. In fact, I’ve benchmarked a JSON API that returns a lot of detail records spending something like 99% of time in AR. Simply wrap the query in PG so that it returns JSON directly, then have RoR fetch and return that 1-col 1-row result directly, POOF! a 10x speedup :wink:

2 Likes

Yeah, we all have to resort to such hacks every now and then I am not objecting to that too much. It’s just that for the 6 years I worked with RoR this has been the rule and not the exception and at one point you start asking yourself “isn’t there a better way?”.

Turns out, there is – Elixir. :smiley: