I'm trying out some parallel programming with Scala and Akka, which I'm new to. I've got a pretty simple Monte Carlo Pi application (approximates pi in a circle) which I've built in several languages. However the performance of the version I've built in Akka is puzzling me.
I have a sequential version written in pure Scala that tends to take roughly 400ms to complete.
In comparison with 1 worker actor the Akka version takes around 300-350ms, however as I increase the number of actors that time increases dramatically. With 4 actors the time can be anywhere between 500ms all the way up to 1200ms or higher.
The number of iterations are being divided up between the worker actors, so ideally performance should be getting better the more of them there are, currently it's getting significantly worse.
My code is
object MCpi{
//Declare initial values
val numWorkers = 2
val numIterations = 10000000
//Declare messages that will be sent to actors
sealed trait PiMessage
case object Calculate extends PiMessage
case class Work(iterations: Int) extends PiMessage
case class Result(value: Int) extends PiMessage
case class PiApprox(pi: Double, duration: Double)
//Main method
def main(args: Array[String]): Unit = {
val system = ActorSystem("MCpi_System") //Create Akka system
val master = system.actorOf(Props(new MCpi_Master(numWorkers, numIterations))) //Create Master Actor
println("Starting Master")
master ! Calculate //Run calculation
}
}
//Master
class MCpi_Master(numWorkers: Int, numIterations: Int) extends Actor{
var pi: Double = _ // Store pi
var quadSum: Int = _ //the total number of points inside the quadrant
var numResults: Int = _ //number of results returned
val startTime: Double = System.currentTimeMillis() //calculation start time
//Create a group of worker actors
val workerRouter = context.actorOf(
Props[MCpi_Worker].withRouter(RoundRobinPool(numWorkers)), name = "workerRouter")
val listener = context.actorOf(Props[MCpi_Listener], name = "listener")
def receive = {
//Tell workers to start the calculation
//For each worker a message is sent with the number of iterations it is to perform,
//iterations are split up between the number of workers.
case Calculate => for(i <- 0 until numWorkers) workerRouter ! Work(numIterations / numWorkers);
//Receive the results from the workers
case Result(value) =>
//Add up the total number of points in the circle from each worker
quadSum += value
//Total up the number of results which have been received, this should be 1 for each worker
numResults += 1
if(numResults == numWorkers) { //Once all results have been collected
//Calculate pi
pi = (4.0 * quadSum) / numIterations
//Send the results to the listener to output
listener ! PiApprox(pi, duration = System.currentTimeMillis - startTime)
context.stop(self)
}
}
}
//Worker
class MCpi_Worker extends Actor {
//Performs the calculation
def calculatePi(iterations: Int): Int = {
val r = scala.util.Random // Create random number generator
var inQuadrant: Int = 0 //Store number of points within circle
for(i <- 0 to iterations){
//Generate random point
val X = r.nextFloat()
val Y = r.nextFloat()
//Determine whether or not the point is within the circle
if(((X * X) + (Y * Y)) < 1.0)
inQuadrant += 1
}
inQuadrant //return the number of points within the circle
}
def receive = {
//Starts the calculation then returns the result
case Work(iterations) => sender ! Result(calculatePi(iterations))
}
}
//Listener
class MCpi_Listener extends Actor{ //Recieves and prints the final result
def receive = {
case PiApprox(pi, duration) =>
//Print the results
println("\n\tPi approximation: \t\t%s\n\tCalculation time: \t%s".format(pi, duration))
//Print to a CSV file
val pw: FileWriter = new FileWriter("../../../..//Results/Scala_Results.csv", true)
pw.append(duration.toString())
pw.append("\n")
pw.close()
context.system.terminate()
}
}
The plain Scala sequential version is
object MCpi {
def main(args: Array[String]): Unit = {
//Define the number of iterations to perform
val iterations = args(0).toInt;
val resultsPath = args(1);
//Get the current time
val start = System.currentTimeMillis()
// Create random number generator
val r = scala.util.Random
//Store number of points within circle
var inQuadrant: Int = 0
for(i <- 0 to iterations){
//Generate random point
val X = r.nextFloat()
val Y = r.nextFloat()
//Determine whether or not the point is within the circle
if(((X * X) + (Y * Y)) < 1.0)
inQuadrant += 1
}
//Calculate pi
val pi = (4.0 * inQuadrant) / iterations
//Get the total time
val time = System.currentTimeMillis() - start
//Output values
println("Number of Iterations: " + iterations)
println("Pi has been calculated as: " + pi)
println("Total time taken: " + time + " (Milliseconds)")
//Print to a CSV file
val pw: FileWriter = new FileWriter(resultsPath + "/Scala_Results.csv", true)
pw.append(time.toString())
pw.append("\n")
pw.close()
}
}
Any suggestions as to why this is happening or how I can improve performance would be very welcome.
Edit: I'd like to thank all of you for your answers, this is my first question on this site and all the answers are extremely helpful, I have plenty to look in to now :)