Akka, Haskell, Erlang, Go and .NET Core compared on 1M threads
github.com
github.com
If we want to compare apple to apples, we should use thread pool shared across Scala actors: http://stackoverflow.com/a/1597942/341181
Just add 7 lines of code and Scala should get at least 10x faster.
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
object FutureGen {
def caller(num: Int, size: Int, div: Int): Future[Long] = {
if (size == 1) {
Future.successful(num.toLong)
} else {
val futures = for (i <- 0 until div) yield caller(num + i * (size / div), size / div, div)
Future.sequence(futures).map(_.sum)
}
}
}
object Root extends App {
// Warmup
Await.result(FutureGen.caller(0, 1000000, 10), Duration.Inf)
val startTime = System.currentTimeMillis()
val x: Long = Await.result(FutureGen.caller(0, 1000000, 10), Duration.Inf)
val diffMs = System.currentTimeMillis() - startTime
println(s"Result: $x in $diffMs ms.")
}
This would give Scala: 499ms and Go: 436ms which is pretty neat, since go actually has no type system. (And I'm using the global execution context, which could also be changed) Currently your akka version is slow, since it's creating a new class on every call, so you would have like a dozen more methods to dispatch than my version. sum := 0
for i := 0; i < div; i++ {
sub_num := num + i * (size / div)
go skynet(rc, sub_num, size / div, div)
}
This is the go version and only the inside will run on a thread so I made a fair comparsion P.S: The go version will actually run single threaded when you are having GOMAXPROCS=1.That betrays the whole benchmark. The basic point of the benchmark is to answer the question, "How expensive is it to create and run 1,111,111 actors in various languages?" You can't make a manual optimization that causes only 111,111 actors to be created and then say that it's a better comparison.
And when you are clever you would be comparing monads, which actually a goroutine could be. and a future/promise, too.
Btw. Even a Future does more than a goroutine do. it won't crash your whole program when it fails, which a goroutine does.
Futures are not a concurrency mechanism precisely. They are a monadic data structure around possibly-concurrent execution. You can wrap a Future around concurrent execution by an Actor, or you can do what you have done and wrap a Future around a sequential recursive computation. As with the synchronous .NET core implementation of the benchmark, the performance is better due to not having the overhead of Actors or other concurrency. Like the synchronous .NET core implementation writing the computation this way is not a fair comparison to the other languages in the benchmark and the high performance does not indicate anything about the efficiency of the concurrency mechanisms available in Scala.
if size == 1 {
c <- num
return
}"c <- num" is a blocking statement. Since c is an unbuffered channel, execution cannot proceed past the statement until a reader consumes a value. The reader in this case creates 10 goroutines executing "c <- num" and only then starts consuming values. If the statements you wrote were inlined execution would immediately halt in a deadlock.
This is a great example. I've done a lot of Scala and o found the original version very obvious to read, but only experience tells me why the compiler can't make it efficient. Your version is not really worse in any way, but I think it's fair to say that it shouldn't be obvious to everyone why one is suboptimal at first glance.
The point is exactly to compare different implementations of the same concept. That's always the point in performance comparisons!
also go is not better than all of these languages. what we learned here, is actually that go is easier since you only have one common way to do this task. go has goroutines, but go doesn't have runnables, futures, actors, completionstages, executioncontexts. GO has GOMAXPROCS and not dispatchers, thread pools, etc..
Also the JVM eve Java only code would actually beat golang in many ways, but thats nothing you should be scared of. Golang is 1 1/2 year old, the JVM lives like 20+ years and they spent numerous times about the GC implementations (they could be even changed so that you could use a better GC for conurrency or for parallelism or for synchronous execution). Go is good for the task it is created. Other languages maybe tend to be more general so that you could do many things and due to their years of knowledge they are actually more optimized than go could be / they could be more optimized than go could be.
Some of us (most of us, I would contend) who have been successfully programming highly concurrent software without them for 20+ years are totally OK with that.
I work on parallel software every day and I would not be OK with having threads and channels as my only tools. I would agree that they're usually the best thing to reach for initially, but it's essential to be able to have low-level control over the scheduling if you want to get good parallel performance out of the hardware. I have workloads that today result in 3x speedups on 4 cores but would become slower than the sequential algorithm on 1 core if I were to rip out the highly-tuned parallel scheduler and replace it with threads and channels.
To be fair, though, I'm working on CPU-bound software and if my workload were I/O bound I'd likely have a different outlook on this.
Java's GC is generational, allowing bump allocation in the nursery. That is a huge advantage.
In the HotSpot VM, you can tune the GC to get any type of max garbage collection pause you want (-XX:MaxGCPauseMillis). This is basic functionality that any concurrent garbage collector supports. The downside, of course, is that pauses will happen more frequently, reducing throughput. That is why just saying "it's under 10 ms, so it's really fast" is very misleading: when your GC is interruptible, you can of course get any max pause time you want. But the GC may not be able to keep up with your allocation rate, and it may start being invoked too often. That's why you need to not just look at latency, but throughput--something that sound bites won't capture.
Pause time is only one metric that's relevant when looking at GC, another very important one is throughput.
In fact you can get very low (10-50 msec) pause times with the latest JVMs even with very large heaps, you can just request it at the command line, but you'll pay for it with slower execution. There is always a tradeoff. Go doesn't have anything fundamentally new in this field.
There is a report here on an adapted HotSpot (not integrated into the mainline JVM though) that gives <10msec pause times at the 99th percentile and an 8 gigabyte heap:
http://welf.se/files/OL15.pdf
But you pay for it with a 15% CPU overhead.Go's GC seems to actually do a full concurrent mark/sweep of the entire heap every single time. That suggests to me that throughput could be rather poor.
Not to mention that you lose bump allocation in the nursery. Nongenerational GC is a lose-lose all around.
But the lowered throughput has to be present as you are scanning the heap and you have enabled the write barrier to make the concurrent scanning work its invariants. In fact, the newest versions will make aggresively allocating goroutines run slower by using some of their timeslot when they request data do run the mark phase.
See the -XX:MaxGCPauseMillis=NNN argument, you might be able to set it to 10ms on highly performant hardware.
Summing a bunch of numbers is a silly task anyways
How a language handles misses/blocking/threading is more important than what it can do in a perfect cpu only world.
Saying the average joe doesn't care to understand what his/her Lang is doing under the hood which will doom them to repeat complexity that many here will respond with "duh" is not justification that a Lang is better - because it supports that behavior "better".
I don't know erlang that well but I'll be damned if I don't spend the time to find out how to properly do M:N with efficient tail recursion.
If one doesn't care for efficiency at all then who cares about benchmarks?
In my comment I purposely mentioned erlang because to implement algorithmic efficiency you must have domain expertise.
"I don't know erlang that well but I'll be damned if I don't spend the time to find out how to properly do M:N with efficient tail recursion."
Tail recursion is an efficiency, doing it right in erlang requires domain expertise.
Building new features and adding value is business. Someone with domain knowledge in any of the langs will be able to implement those business values in the best possible way.
If your business is built with shitty coders doing shitty things you will eventually find yourself in hot shit. Even if it takes 5 years. What comes around goes around.
I would take the domain knowledge developer over any monkey anytime. That's your 10x developer. That's who built all that awesome open source stuff you use all day. Not the business feature value guy.
He builds businesses that fail 99% of the time. That other guy building that obscure Lang which turns out useful for that niche topic (I.e concurrency) lives on even in concept to build or inspire other langs/ers.
You see, that domain expert knows to pick the right algorithm for the right job.
He knows the right Lang for the right job.
Even if that job is only business value.
Long live domain knowledge experts and those who wish to attain it.
Also check out Pony if you want something were spawn and send become basically free. It'll run pretty close to for-loop speed for the number adding which should be a baseline calculated in all implementations.
Indeed, see ".net sync" version. The point is to measure process spawn/message passing performance.
For those who don't know what you're referring to, an example:
http://benchmarksgame.alioth.debian.org/u64q/performance.php...
Those things never describe real workloads - in such a view a Porsche is always better than a truck because it accelerates faster. Tough luck if the workload is shipping 20 tons of manure. Put that in your Porsche.
https://github.com/RayRacine/skynet/blob/haskell/haskell/Sky...
https://github.com/atemerev/skynet/blob/master/erlang/skynet...
EDIT: it seems that list:seq overhead is insignificant - so it doesn't matter, maybe erlang compiler already optimizing it. I even inlined the calls to spawn - it still the same result. But I found another problem: it seems that some processes are stuck - so if you run benchmark the 3rd time - yo get out of processes.
EDIT2: Note, that unlike other solutions. Erlang has process isolation, i.e. if there is a panic in one of your goroutines - your entire server will crash, but if there is an exception in your erlang process - only that process will crash, not affecting other processes in your server (unless they linked with it).
There's probably an alloc cost, but there should be no GC cost: erlang processes get a 233 words heap by default, a list of 10 integers is 21 words, so the process should die without needing to run GC.
Yeah whether using spawn_opt or VM options (-hms and -hpds) it doesn't look possible to reduce the process heap (or process dict) below the initial size, only to increase it.
and speed doubles :)
Of course the results are more or less expected. JVM is by far the worst, because it uses OS-level threads, while Go and Erlang use userland threads, which are way cheaper to spawn and manage.
[edit] According to some other comment around here, Scala is supposed to use userland threads as well.
Yup.
However, Actors are not threads, so in a way it's like user threads. One million actors will be scheduled to run on a pool of OS threads.
Moreover, the core assumption that native == faster is not true. One advantage of the jvm is that as a program runs, the runtime can internally profile and adjust the byte code on the fly in order to increase performance.
Please note that Windows and OS X is benchmarked on different CPUs!
You're welcome to re-test at dual-boot MacBook together so we can meaningfully compare OS X/Windows
Testing with allocation in all languages will give a better idea of performance (and I won't be surprised if .Net core fares very well).
I think there are by far too many knobs to turn to make this comparison meaningful.
1. That the time (including start-up time) for something on the JVM would be longer because the JVM needs to start up.
2. That Go producing a native binary is automatically faster at things.
In reference to the first, the benchmark timings are started when the software actually starts running.
In terms of the second, being a native binary doesn't mean faster. Go still has a runtime. It's just included in the binary.
Go's performance advantage is probably a few things. Go was made to spawn millions of goroutines and Go uses primitive types. I don't know all that much about Scala, but I believe it doesn't do primitive types which means that every one of those Int objects requires following a pointer to get to the value. That's overhead. EDIT: this looks like it's wrong as Scala has a bunch of pre-defined classes that extend AnyVal and correspond to primitive types in Java.
It also looks like the Scala code is passing back a Long even when the value is still small enough to fit in an Int. That's doubling the amount of memory when that memory only needs 64-bits near the end stages. By contrast, the Go code does `sum := 0` which I'm not actually sure how that's working. If I initialize a 32 bit int without specifying it, I see that I wrap around (http://play.golang.org/p/PiiMZmEzKp). So, I'm not sure how initialising it to 0 and then adding to something over an int32 works. Maybe Go uses a different default int depending on platform?
Go's implementation is also simpler. In the Scala version, there's a receive method and the sub-actors just pass back when they're done potentially causing a bunch of switching. Go, on the other hand, creates all the goroutines and only when that's done starts receiving from them. In the Scala version, the program might think that there's value in switching from creating the sub-actors to receiving from them - and they should pass back very quickly. The implementation here in Go doesn't allow for that switching.
When dealing with something as synthetic as this, all of these little things matter.
No? Erlang and Scala both have JITs. The JVM does quite a bit more optimization than Go's compilers do.
The net result is that Go is relatively efficient for naive programs that start hundreds or thousands of threads and let them all run.
I don't know Scala or Erlang, but based on the results I would assume both are allocating a posix thread for each thread in the software and so it is using the kernel scheduler to switch between them with all the overhead that entails.
But a program can be refactored in all the languages to have a smaller number of workers and a queue of work to be consumed. This would be faster than the Go version, but require more code and complexity.
I have been considering threading in Rust vs Go and that is the biggest difference I see between the approaches. However it should be possible to implement Go style goroutines in Rust using custom dispatch code like old C coroutines. I think I have already seen libraries like that, but I haven't dug in the details to know for sure.
Erlang does use lightweight processes, but the configuration isn't tuned for high creation/destruction throughput: on my system, the smallest possible process is 333 words, or 2.5Kb (control structures, default stack and default private heap)
Coming from an Erlang world to akka, this blew my mind.
No. Erlang is only ~five times slower than Go, according to the benchmark run, which is a marvellous result given the Erlang's VM is slow as hell, even with HiPE. Scala/Akka is slower by thirty times under this workload.
> But a program can be refactored in all the languages to have a smaller number of workers and a queue of work to be consumed. This would be faster than the Go version, but require more code and complexity.
Oh, I bet it wouldn't be faster, but it surely would add tons of complexity. Erlang was specifically designed for programmer to spawn a thread for each activity. New client connected? Spawn. Need a background computation? Spawn. Need a connection to external entity? Spawn. Need to do cleanup (reliably) after this computation terminates? Spawn. Go shares this approach to some extent.
Incorrect.
> I have been considering threading in Rust vs Go and that is the biggest difference I see between the approaches. However it should be possible to implement Go style goroutines in Rust using custom dispatch code like old C coroutines. I think I have already seen libraries like that, but I haven't dug in the details to know for sure.
There is https://github.com/dpc/mioco for I/O-based coroutines. For CPU bound tasks you want something like https://github.com/nikomatsakis/rayon.
After this change my results changed from: Result: 1783293664 in 61586 ms. to: Result: 1783293664 in 28401 ms.
And just to be clear, the interesting property to test is parallelism, not concurrency.
I wrote a py3.5 asyncio coroutine implementation of this benchmark, and I hope it's just my inexperience with asyncio that makes it get such horrible results.
Here it is: https://github.com/ociule/skynet/tree/master/python35-asynci...
http://benchmarksgame.alioth.debian.org/u64q/performance.php...
$ git show --no-patch --pretty=oneline HEAD
82f318ad418aa10eb5553bcbfad1783e544c76cd Merge pull request #26 from bitemyapp/master
$ java -version
java version "1.8.0_11"
$ go version
go version go1.5.3 darwin/amd64
$ (cd scala; sbt compile run)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=384m; support was removed in 8.0
[info] Set current project to skynet (in build file:/Users/twic/Documents/Code/skynet/scala/)
[success] Total time: 1 s, completed 15-Feb-2016 22:59:34
[info] Running Root
Result: 499999500000 in 8832 ms.
Result: 499999500000 in 4866 ms.
Result: 499999500000 in 2457 ms.
[success] Total time: 17 s, completed 15-Feb-2016 22:59:51
$ (cd go; go run skynet.go)
Result: 499999500000 in 399 ms.
$ (cd java; ./gradlew run)
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
Streams
-------
Result: 499999500000 in 18 ms. (sequential)
Result: 499999500000 in 38 ms. (parallel)
RxJava
------
Result: 499999500000 in 204 ms. (immediate)
Result: 499999500000 in 216 ms. (computation)
Result: 499999500000 in 188 ms. (io)
BUILD SUCCESSFUL
Total time: 10.784 secs
Get off my lawn.Code is pretty-much copy pasted from .NET Async version with bluebird promises instead of Tasks. Ends up being almost 2 times faster:
https://github.com/atemerev/skynet/pull/16
(Only 50% faster if we return Promise.resolve(num) though)
Lwt is a cooperative multi-threading library with a particular semantic: A task that doesn't block doesn't even yield, it returns directly.
Yes, it's comparing apple to oranges.
edit: and the erlang bench allocates a bunch of lists on startup: https://github.com/atemerev/skynet/blob/master/erlang/skynet... `lists:seq()` will allocate a 10-elements linked list, considering this is called recursively and concurrently it might contribute to the issue (or not)
And Python is compiled to bytecode and executed on the CPython virtual machine. That's still interpreted.
Were these processes/goroutines to do anything more complex, then the difference goes toward zero. But the constant overhead at the lowest level is probably higher.
[0] the way I understand it, each process would generate a 10-elements list of small integers, which would be 21 words (1 word + 1 word/element + the sum of the element sizes — 1 word each for small integers)
Anyone know if that would be a problem?
Second: using the default thread dispatcher for this kind of scenario is pretty dumb on scala (also I think erlang could be made faster, too. but since I'm not an erlang guy...) Fixed Thread Pool (GOMAXPROCS vs ForkJoin)
Warmup's a tricky issue, see this brand new paper: http://arxiv.org/abs/1602.00602
My laptop runs the original in about 3.07s and my improvements bring it down to 1.20s though I think there's room for improvement.
[EDIT] My improvements were actually not the same algorithm as the original's, with algorithm corrections my version is actually slower!
He says, with absolutely no profiling whatsoever... I would like someone to offer some evidence though :)
$ ./skynet
Result: 499999500000 in 294ms.
I am having hard time understanding what you are trying to test here. I am worried that .NET code is not really testing what you think it is.
[1] https://msdn.microsoft.com/en-us/library/dd997402(v=vs.110)....
Maybe you mean we should schedule something which has a complete event loop? That would be a lot more fair wrt the Akka version, but the Go version does not create any threads either.
I don't know any compiler (and language implementation) that can do that, though.
In general programmers are pretty good about not kicking off expensive computations they don't need to, so the easy pickings here are probably pretty small. And as soon as there are any side effects at all (which includes any writes to shared memory) it becomes very hard to reason about moving computation from one thread to another. So it's easy to see why compiler writers are not super excited to start optimizing across concurrent threads.