Before asking for performance improvements, have you tried:
1. tuning the BEAM? It has a lot of knobs. For example, there's "initial heap size", which can effectively turn any subset of short-lived processes that would otherwise need to do GC into simple "allocate an arena, do work, deallocate the arena" workloads.
2. tuning your OS? WhatsApp had to tune their (FreeBSD) kernel quite a lot to get it delivering packets to the Erlang runtime efficiently.
3. disabling tracing support? Part of the reason ERTS focuses on being a bytecode interpreter, rather than an AOT compiler, even in production, is that this allows ops engineers to connect to production nodes and trace/debug/profile code that's not doing what they'd expect, watching what happens as real user data flows through it. Because of this, the interpreter is "instrumented" even when a "production" (embedded, etc.) profile is used to generate a release. If you aren't going to use this tracing support, though, you can tell the runtime to globally disable it—for big wins in interpreter speed. (I separate this from #1, because this is basically the last-resort BEAM knob to turn. You're throwing away a lot of cool stuff by doing this.)
3. using HiPE? Specifically, sticking `@compile {:erl_opts, [native: :o3]}` into a subgraph of your modules (like a parser, a lexer, and the code that calls them both) can yield major improvements.
4. really using HiPE? I.e. recompiling both OTP and Elixir, and your project, using `ERL_COMPILER_OPTIONS="[native,{hipe, [o3]}]"`. This makes some things slower, so it's not the default, but it can beat #3 for modules that make a lot of stdlib calls (e.g. anything that loops using calls to Enum/:lists.)
4. using NIFs? (I don't recommend going this route until you've found that all of the above fail to get you enough performance, because the above all let you continue to just write plain Erlang/Elixir, while this step requires worrying about FFI. But it can be a big win, where and when it makes sense, just like in any other interpreted language.)
If your Erlang code is still slow after all of those, then I'll be surprised and would like to hear your specific story.
Enum.reduce(rows, %{}, fn [user_id, hits], agg ->
Map.update(agg, user_id, hits, &(&1 + hits))
end)
I didn't apply any of your suggestions.If I had another performance-like related pain point, it's throwing multiple cores at some data. I think it's somewhat common to have a large piece of data that you want to concurrently access. For example, keeping a large suffix tree in memory for an autocomplete endpoint, and letting each request concurrently access it. This isn't really a specific performance issue, but it's a performance sensitive pattern that is very antithetical to everything Erlang. ETS has limited structures, requires copying of values, and has no atomic replace (so you can't build a new version in a temp table and just rename it).
Most erlangers would not use HiPE in production.
The 'let it crash' is really something I've not encountered in any other programming language/environment.
I've got a constantly growing Elixir project that I use for scratching various itches. Quite often I'll prototype some new feature in some of my free time, and quite often what I come up with is pretty haphazard.
In some cases the stuff I have running crashes for the silliest of reasons, but the rest of the app is unaffected, so it doesn't matter. A look at my log often makes it clear that I forgot something as stupid as checking for a timeout to a HTTP request. I fix the issue, push it to the server, recompile in the IEx console (or Application.start()/stop()), and keep an eye on what happens next.
When I have a bit more time, I might actually take a more careful look at the code to fix some obvious issues that somehow haven't caused problems, or I might add some typespecs when things seem to have solidified, where Dialyzer/Dialyxir, via its 'success typing' (which I guess is basically gradual typing like TypeScript?) will probably help me avoid stupid mistakes down the road.
What I like is how organic the whole process feels. The godawful, badly thought out parts of my project don't cause the whole thing to crash, and the more solid parts can be augmented by a type system. It's an approach that I was previously entirely unfamiliar with.
Actors are all about managing state. It's when state starts leaking around or being modified unexpectedly ('wait, why is that being called there?!') that a more typical language (Java, say) starts to get tightly coupled, and sharing that state is the norm, and expected. And I agree, if you have pure functions, that issue is largely controlled for too. Just, that's a really hard paradigm to stay inside of and still be productive.
I think that's why microservices are even a thing; it forces you to decouple state from different 'functional' parts of your system; by creating separate systems you have to be very intentional about what state you share, and how you share it (database or redis or an API call or similar depending on need).
Actors do that automatically for you; the state is contained within them; there's no chance(1) it will accidentally be changed by some other process/code, and the 'controlling' process gets to decide what messages affect changes, and how those changes occur. Or, in other words, all state changes relating to a context happen in one place, rather than state being passed around to be changed anywhere willy nilly across contexts.
1 Technically there are system level things that allow you to munge state inside of an actor. This is more frowned upon than using reflection for arbitrary reasons in Java, however, so the only reason you'll ever do it is because you're debugging what's going on in something, or you have decided you absolutely -hate- your toes and need to footgun them off.
Messages tend to be lightweight. Often a symbol/atom "command", and a few scalar values. Like command line arguments. If the process needs more data, it sends its own message to the owner of said data.
With conventional functions and OO, you're often dealing with complex objects (with references to references ...) which often leads to breaking the Law of Demeter.
Message passing also gets your asynchronicity. You're also decoupled from specific runtime implementation (the receiving can be on a different machine, the semantics are the same). You can achieve the same without message passing, but it's certainly more baked-into the runtime/tooling/language.
Joe Armstrong's popular quote on this:
"The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle"
Messages and function calls aren't quite isomorphic, but they're pretty close. (They get really close if you include coroutines and coroutine-like things, to cover the non-1-to-1 relationship between messages and replies that messages can have.) I'm not sure the difference can sustain the implication you're trying to draw. Especially if you include RPC in the function world.
Problem = tight coupling
Solution = x, where x => {indirection, implicit_coupling}
I'm not sure I agree the final judgement call, but the analysis is spot-on. Message-passing interfaces are generally difficult to discover and to debug, and require a lot of careful documentation, logging & instrumentation.You just use the "most common structures". For example, Elixir has the Agent module, which encapsulates state, but lets whatever you're using interact with the state via lambdas (or passed functions).
Another common structure is the state machine. I was somewhat displeased with the complexity of gen_statem, so I wrote my own! It's about 50 lines of code, and can have plugins to do facilities like logging and debugging via a plugin-able interface.
Ultimately the real win with message-passing is not having to write spinlocks, semaphores, or mutexes.
myActorModule.increment(ref, amount) -> ref ! {increment, amount}
myActorModule.syncIncrement(ref, amount) -> ref ! {increment, amount, self()} receive -> ok timeout 1000.
You can build out the message passing interface to an actor -as- functions, and even supply sync ones if you want. The actor implementation can be completely opaque to the caller; just the exported interface functions are defined, exported, and supported.
That said, you'll run into issues once you hit a certain level of scale. I've had to make patches to the BEAM just to keep us scaling, and I've never seen someone get away with stock dist. You pretty much have to rewrite pieces of OTP.
Now is Elixir ok for a CRUD app serving 10G of traffic? Of course... but this is also the age in which every language has a solution to the C10K(, C100K, and more in many cases) problem, and there are compelling reasons to write your CRUD app in Go and get better library support, static types up front (trust me, you want a static type system), and lower latency.
Personally, I'm bullish about Rust. With async/await landing, it stands a good chance of providing the same focus on safety and reliability while being infinity more flexible at what it can effectively. It's a better general purpose language. Also, the actor model (and CSP as well) aren't the end all be all of concurrency patterns. Rust only provides the building block (futures) so it's much easier to choose the concurrent pattern that fits best for your problem. You can sting a future using the actor model, so it's still an option, but you aren't limited to it.
Elixir gives you that plus a lot of goodies that are really hard to get in other languages. The creators of Erlang were really clever and in my opinion, so are the creators of Elixir. If I ever were "bullish" on a language, Elixir would be it.
Elixir is a very pragmatic language. It doesn't get in your way, but still forces decent habits and design on your application.
Erlang has historically been a bad choice for this, and there's been a lot of effort (dialyzer, records, maps etc) to make it better.
Dynamically typed languages are, in my experience, not worth the cost of maintenance and readability. In Elixir and Erlang it's possible to create function signatures that tell you almost nothing about the shape of the data you expect to pass in. This makes it really hard to collaborate at scale. Various attempts to enforce dialyzer use are usually met with 'we need to move faster' or 'we'll add them later'.
So if you want to write bug-free readable code, I think Rust is a much better choice. It's statically typed, and the addition of traits and lifetimes tell you so much about how a function or type will behave, and there's no need to sync specs or docs. The code doesn't lie.
On the contrary I struggle to get even the most basic Rust program to compile, wasting hours before anything is done.
BEAM languages are fine, easily faster than Python, Ruby, PHP, and competitive with v8 in many cases. Furthermore anyone doing serious data processing in a web service uses a work queue, which can be serviced (if you so choose) by workers written in any language you like.
Modern Python, Ruby, PHP7 are all much faster than generally expected outside of microbenchmarks.
It was pretty annoying to read.
I have not tried doing it, but I'd imagine the safety guarantees of Rust are a nice thing to have to reduce the risks involved around NIF callouts. I believe Discord recently posted that they are doing this, and I'm interested in hearing about their results.
edit: Google is my friend. https://github.com/hansihe/rustler
https://www.youtube.com/watch?v=F86JXaCPowo
(Sonny is a core team-member of the Phoenix framework).
It's not a good fit for the way in which we're doing fault tolerance in the cloud. Erlang focuses on not crashing the BEAM, by letting processes crash while allowing graceful degradation of service. Supervisors are akin to Kubernetes deployments. Processes are pods. The pods themselves should be designed to crash gracefully.
You could probably do this with a static type system. I wouldn't want to. I'm not an "expert" elixir programmer, and I got this up and running in 2 hours.
You feel that way because you did :)
If you don't enjoy writing Elixir, you could always just install Elixir, and then write Erlang that calls into Elixir-stdlib modules.
I believe you can even install Elixir packages into an Erlang rebar project, if you like. No need for Mix or anything.
Whether you consider Elixir fast or slow probably depends on what you're coming from and what you're doing. Personally, I wish the performance story would significantly improve.
But, the reason I think Elixir should be your goto language comes down to process isolation and the ecosystem around it (the synergy of the runtime, libraries and language). If you're doing a pure web app, you might not fully leverage it. But, for anything that involves long-lived in-process data (like a workflow/pipeline, or persistent connections), the way to approach this in Elixir (with supervisors, processes, message passing and pattern matching) tends to result in highly cohesive and maintainable code. Message passing is a pretty effective antidote to coupling.
I just finished writing code that had to take a stream of unordered time-based data and dedupe it over a small window. I'm not overly pleased with the final result, but it's completely isolated from the much larger app it sits in, easy to refactor/rewrite, with no risk of someone taking a shortcut and just accessing some internal detail, and testable.
I feel like I wrote a microservice, but without any of the challenges of a separate app.