Testing Akka Performance
Few weeks ago I attended a workshop called “Understanding Mechanical Sympathy” ran by Martin Thompson. During that workshop we written and tested few concurrent programming techniques and as a first exercise we have written a simple Ping-Pong program:
package uk.co.real_logic;
import static java.lang.System.out;
/*
Original exercise did during "Lock Free Workshop" by Martin Thompson: http://www.real-logic.co.uk/training.html
*/
public final class PingPong {
private static final int REPETITIONS = 100_000_000;
private static volatile long pingValue = -1;
private static volatile long pongValue = -1;
public static void main(final String[] args) throws Exception {
final Thread pongThread = new Thread(new PongRunner());
final Thread pingThread = new Thread(new PingRunner());
pongThread.setName("pong-thread");
pongThread.setName("ping-thread");
pongThread.start();
pingThread.start();
final long start = System.nanoTime();
pingThread.join();
pongThread.join();
final long duration = System.nanoTime() - start;
out.printf("duration %,d (ns)%n", duration);
out.printf("%,d ns/op%n", duration / (REPETITIONS * 2L));
out.printf("%,d ops/s%n", (REPETITIONS * 2L * 1_000_000_000L) / duration);
out.println("pingValue = " + pingValue + ", pongValue = " + pongValue);
main(args);
}
public static class PingRunner implements Runnable {
public void run() {
for(int i = 0; i < REPETITIONS; ++i){
pingValue = i;
while(i != pongValue){
}
}
}
}
public static class PongRunner implements Runnable {
public void run() {
for(int i = 0; i < REPETITIONS; ++i) {
while (i != pingValue) {
}
pongValue = i;
}
}
}
}
As you can see we have 2 threads that need to synchronize on the single variable, this is achieved by checking the value of the variable and if it matches expected value, the thread can continue. What’s worth to note is that those 2 threads are busy spinning (inside while loops).
Testing Original Code
After running the code for few minutes the test results stabilize in my case (i7-3610QM) around those values:
duration 10,756,457,024 (ns)
53 ns/op
18,593,482 ops/s
After pinning threads to specific CPU cores the results are slightly better:
duration 10,147,866,952 (ns)
50 ns/op
19,708,575 ops/s
This pretty trivial optimization, helped to gain about 7% of performance gain, so overall not much (I suspect that my Linux system is performing some sort of optimizations at this level as well).
Akka version
After playing around with the pure Java version of the code, I decided to see how I could design a close enough Akka version of this exercise and what optimizations I could use to improve it’s original performance.
This is my code:
case object Ping
case object Pong
object PingPongAkkaApp extends App {
override def main(args: Array[String]): Unit = {
val t = new Tester
t.run()
main(args)
}
}
class Tester {
val REPETITIONS: Int = 100000000
val startTime: Long = System.nanoTime
val pongValue: AtomicInteger = new AtomicInteger(0)
def run() = {
val system = ActorSystem("PingPongSystem")
val pongActor = system.actorOf(Props(new PongActor(REPETITIONS, pongValue)), name = "Pong")
val pingActor = system.actorOf(Props(new PingActor(REPETITIONS)), name = "Ping")
system.registerOnTermination {
val duration: Long = System.nanoTime - startTime
printf("duration %,d (ns)%n", duration)
printf("%,d ns/op%n", duration / (REPETITIONS * 2L))
printf("%,d ops/s%n", (REPETITIONS * 2L * 1000000000L) / duration)
println("pongValue = " + pongValue)
}
pongActor.tell(Ping, pingActor)
Await.result(system.whenTerminated, Duration.Inf)
}
}
class PingActor(repetitions: Int) extends Actor {
override def receive = {
case Pong =>
sender ! Ping
}
}
class PongActor(repetitions: Int, pongValue: AtomicInteger) extends Actor {
var counter: Int = 0
override def receive = {
case Ping =>
counter = counter + 1
if (counter >= repetitions) {
pongValue.set(counter)
context.system.terminate()
} else {
sender ! Pong
}
}
}
In this case instead of explicitly synchronizing on the variable we use 2 akka actors to send messages between each other. The PingActor
just replies with Ping
to each Pong
it receives, but PongActor
stores the counter and can stop the actor system when it reaches expected number of iterations. In this case we also need a little bit more boilerplate code to setup actor system and wait for termination.
Testing Akka Version
Let’s look at performance results after running this code without any additional configuration:
After running test for couple of minutes the results stabilized around those values:
duration 222,328,154,119 (ns)
1,111 ns/op
899,571 ops/s
As you can see it’s roughly 20x slower than Java version, that’s quite bad.
During test execution I noticed that it was using all my CPU cores at 100% which was undesired so I decided to add 2 things:
Setup proper Akka configuration, in this case something like that was enough:
akka {
actor {
default-dispatcher {
type = Dispatcher
executor = "thread-pool-executor"
throughput = 1
fork-join-executor {
parallelism-min = 2
parallelism-factor = 0.5
parallelism-max = 3
}
}
}
}
Additional I’m pinning Java process to use only 2 CPU cores:
taskset -c 1,2 java -server -jar target/scala-2.11/akka-ping-pong-assembly-1.0.jar
This gave much better test results:
duration 60,581,436,734 (ns)
302 ns/op
3,301,341 ops/s
Summary
As you can see the overhead is now 6x compared to Java version which is not bad when taking into account all additional work that happens in Akka to provide all additional features we don’t have when using pure threads.
I’m sure it’s possible to go even further with optimizations and I suspect that this time could be cut by additional 50% given enough experimentation, but I’ll leave as an exercise :)
Here is the git repository with full code: https://github.com/wlk/akka-ping-pong