Scala on .Net


 

Technorati Tags: ,

Introduction

This post gives an overview of Scala from a C# developer’s perspective in light of efforts for a current .Net port coming closer to fruition. By closer I mean they have the compiler done but not the Visual Studio or SharpDevelop plug-in. From what I could find there is also no CLR based REPL available. You are also unable to call CLR code with generic signatures. While all of these issues are being worked on, to have a play I suggest using the JVM implementation and either IntelliJ or Eclipse as the IDE – both of which provide free options. Consider this post a peek at what will (hopefully) be available soon for the CLR.

Why Scala?

On the JVM Scala has a very strong case indeed with Java language level innovation moving at a glacial speed. Things such as Lamda expressions, LINQ, automatic properties, extension methods, the ability to overload an operator are mere wish list items for disgruntled Java developers. With Scala however there is an equivalent to all of those features and more.

On the CLR the case is not so clear cut given its current state of interoperability and F# filling the object / functional hybrid language gap very nicely. The following are some reasons to consider Scala on the CLR once it is a first class .Net language:

  • If you want to write a cross platform application.
  • If you want to provide a DSL like API. Scala’s very flexible syntax allows you to create DSLs in much the same way as you can in Ruby. The difference is that Scala is statically typed.
  • If you would like to take advantage of some of Scala’s advanced APIs available in the Scala library. For example the Actors API.
  • The Scala open source community is very active so ports of popular Scala projects may become viable.

Getting Started

As at the time of writing being productive in Scala still means using the JVM version and using the workaround documented in the Preview of Scala.Net document to get code completion for CLR types in the Java IDE. So grab yourself the current version of the Java JDK, then the Scala Distribution and a Java based IDE. The sample I’ll work through below was put together using IntelliJ and then compiled to the CLR from the command line.

As with many functional languages, Scala offers a REPL from the console where you can experiment.

C:\JavaApps\scala-2.9.0.1\bin>scala
Welcome to Scala version 2.9.0.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_18).
Type in expressions to have them evaluated.
Type :help for more information.

scala> val numList = List(1,2,3,4,5,6,7,8,9)
numList: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

scala> val evensList = numList.filter( x => x % 2 == 0 )
evensList: List[Int] = List(2, 4, 6, 8)

scala> val conciseEvens = numList.filter( _ % 2 == 0 )
conciseEvens: List[Int] = List(2, 4, 6, 8)

scala> val someXML = <outterNode>some content
     | <innerNode attr="value"/>
     | </outterNode>
someXML: scala.xml.Elem =
<outterNode>some content
<innerNode attr="value"></innerNode>
</outterNode>

scala>

In the REPL session above we first create a list of Int object – the numList value. Lists in Scala are linked lists similar to what you might work with in F#. Note how we didn’t have to use the new keyword or specify the generic type directly. Once you type in your expression the REPL responds with its type and value. Generics in Scala use the square bracket instead of the angle bracket. Angle brackets are used for XML literals. The pipe symbols are put there by the REPL when it detects you’ve typed an incomplete expression and a new line. The val keyword is used to define an immutable value. The lists above are all immutable including their content. To define a variable use the var keyword.

The lamda expression used for the evensList value is basically the same as you’d see in C#. A short hand version is also available in Scala that uses the underscore character without the parameters and the => symbol as shown with conciseEvens. The underscore can only be used if a lamda parameter appears once in the expression body. So the mental substitution to make is to add a parameter per underscore character you see in any Scala lamda body.

Language Overview

Scala is a statically typed general purpose language that originated on the JVM. It has had a CLR version for a while but has only recently aimed at supporting the current version. It was designed from the outset to be a scalable language (hence the name Scala), providing the facility to write anything from small scripts to large enterprise systems. The language is conceptually a blend of object-oriented and functional programming allowing the developer to make use of the most appropriate programming style for the problem at hand.

Syntactically it shows its Java heritage with a lot of C style syntax, but it also borrows from Pascal with its variable declarations within methods and functions and from Haskell for many of its functional idioms. Many of the verbose aspects of Java/C# are legal in Scala but are optional and not idiomatic. For example:

   1: def factorial( x: BigInt ): BigInt = {
   2:     if ( x == 0 )
   3:         return 1;
   4:  
   5:     else
   6:         return x * factorial( x - 1);
   7: }

 

can be more idiomatically expressed as

 

   1: def factorial( x: Int ) = 1 to x reduceLeft( _ * _ )

 

As you can see much of the ceremony of C# and Java such as the curly brackets, semicolons and the return statement are unneeded. There’s also more to the above than first appears. Or really it’s less when you consider there are very few language level tokens. The to operator appears to be a key word when in fact its a method (the . can be replaced with a space and the parenthesis are optional for single parameter methods) on the RichInt class that returns an Inclusive instance which is a sequence. This in turn offers the reduceLeft method that takes a function parameter to reduce the sequence 2 elements at a time from the left. Recall that the underscore character is a place marker so above we have 2 parameters that are multiplied by each other giving us our factorial result. The number 1 was converted to a RichInt using an implicit method. Implicit methods are similar to C# extension methods. It’s this kind of language level flexibility that allows Scala APIs to provide a DSL like syntax without the requirement to create a parser.

Traits

One of the most interesting features I found in Scala is traits. I mention them in detail because they feature heavily in the sample code below. Traits are Scala’s fundamental unit of code reuse. A class can mix in any number of traits to implement a contract in the case where a trait defines abstract methods. This is the equivalent of an interface. Or a class can make use of the extra functionality provided by a trait in the case where a trait defines concrete methods.

Traits differ from C# interfaces by encapsulating method and field definitions rather than just methods. The method definitions can either be abstract or contain full implementations. Functionally they are the same as classes with the following differences:

  • Traits cannot have any class parameters. Scala class parameters are the equivalent of constructors with arguments in C#. A specialised syntax exists that allows you to pass a value to an abstract field.
  • In traits, base calls (using the super keyword) are dynamically bound where as with classes they are statically bound. That is if you write super.someMethod() in a trait the exact method that is called is not necessarily known until runtime. This odd behaviour is what allows traits to work as stackable modifications.

The Sample

The sample I’ll work through models a simplistic automated underwriting mechanism from the domain of commercial insurance.

As part of a policy’s lifecycle the details of the policy are evaluated against certain limits or attributes. If the limit is under a certain amount no intervention is needed and, for that part at least, the policy is rated against the base premium. Over a certain amount the policy requires manual attention from an underwriter and is said to require a referral. Over a higher limit and the policy is declined outright again with no manual intervention. The types of things that get insured and therefore the attributes that are the subject of underwriting vary greatly but there are cases where policies of different products are similar.

The example I’ll use is from cargo insurance with the variants of single cargo and annual cargo. We will use traits to model the individual referral rules and inheritance to model the policy types themselves.

We will start with a simplified domain model. I’ve shown all model classes for brevity. As with C# you can place all of these in a single file if you wish.

   1: package com.maslen.scala.policy
   2:  
   3:  
   4: class Policy {
   5:     var referrals = List[ Referral ]()
   6:     var lossHistory = List[ LossHistory ]()
   7: }
   8:  
   9: class CargoPolicy extends Policy {
  10:     var storagePeriod: String = ""
  11: }
  12:  
  13:  
  14: case class LossHistory(lossYear: Int, lossAmount: BigDecimal)
  15:  
  16: case class Referral (code: String, description: String)

 

Class definitions are similar to C# with a few exceptions:

  • Fields are public by default and are always exposed as methods under the covers. Scala doesn’t require the parentheses when accessing a zero parameter method, thus giving the effect of C# properties. So you can later redefine your field as a zero parameter method and no calling code will be broken. If you need to redefine the setter you name the method with a _= suffix. eg def referrals_= ( referrals: List[ Referral ] ).
  • If a class requires no body you don’t need the curly braces.
  • Constructors are represented as parameters to the class as shown in the LossHistory and Referral classes.
  • the case keyword is used to create case classes. In short they allow pattern matching on a type. I’ve used them here because they provide automatic implementations of the toString and equals methods making printing for the example easier. You also don’t need the new keyword to instantiate an object from a case class. This can make your code appear more like a DSL.

Now to the referral implementation. The first of these classes is an abstract class that defines the interface for automated referral for all policies:

   1: package com.maslen.scala.policy.underwriting
   2:  
   3: import com.maslen.scala.policy.{Referral, Policy}
   4:  
   5: abstract class AutoReferral {
   6:   type P <: Policy
   7:   var referrals: List[ Referral ] = List()
   8:  
   9:   def addReferral(referral: Referral) {
  10:     referrals = referral :: referrals
  11:   }
  12:  
  13:   def applyReferral(Policy: P)
  14: }

 

An abstract class definition looks and works similarly to a C# abstract class except abstract methods and fields do not use the abstract key word. Abstract methods simply have no implementation and fields are abstract if they are unassigned. The rest of the class is explained below:

  • Imports in Scala offer a more fine grained mechanism than the C# using declarations. So above we import the Referral and Policy classes from the policy package.
  • Scala allows you to define alias's for a type using the type keyword. Here we define the P alias for any subclass of the Policy class (the use of the <: symbol. This same symbol is also used to indicate restrictions for generic types). Classes that extend AutoReferral can define the type of P providing the ability to override the applyReferral method but at the same time use a concrete parameter type for policy. This then gives us the ability to access properties of the sub type without down casting.
  • Lists in Scala are immutable by default. For a persistable class we're more likely to use a mutable List, but for the example I've chosen to use the immutable List. So to add an element you need to reassign to the original list as you would with say a C# String. The "::" you see that appears to be an operator (pronounced "cons") is in fact a method on the List class. Scala treats methods that end in the ":" character differently in that they are right associative. So the :: method is on the List class not the Referral class. Ultimately the line referrals = referral :: referrals is pre-pending a Referral object to the referrals List. The item is prepended because a Scala list is internally represented as a linked list. Prepending a linked list is a zero cost operation where as appending gets slower as the list grows.
   1: package com.maslen.scala.policy.underwriting
   2:  
   3: import com.maslen.scala.policy.CargoPolicy
   4:  
   5:  
   6: abstract class CargoAutoReferral extends AutoReferral {
   7:     type P = CargoPolicy
   8:  
   9:     override def applyReferral(policy: CargoPolicy) {
  10:         policy.referrals = policy.referrals ::: referrals
  11:     }
  12: }

 

The CargoAutoReferral class extends the AutoReferral Class providing the following:

  • A concrete assignment of the P type alias to CargoPolicy.
  • A concrete implementation of the applyReferral method. This method concatenates the two lists together (the ::: method).

Now we've defined our Cargo Policy specific automated referral class we can create traits that operate on CaroPolicy types and conditionally add referrals. The first of these is the ExceededLossReferral.

   1: package com.maslen.scala.policy.underwriting
   2:  
   3: import com.maslen.scala.policy.{Referral, CargoPolicy}
   4:  
   5: trait ExceededLossReferral extends CargoAutoReferral {
   6:     val exceededLoss: BigDecimal
   7:  
   8:     override def applyReferral(policy: CargoPolicy) {
   9:         val totalLoss = policy.lossHistory
  10:             .foldLeft(BigDecimal(0))(_ + _.lossAmount)
  11:  
  12:         if ( totalLoss > exceededLoss )
  13:             addReferral(new Referral("R001", "Total loss limit exceeded"))
  14:  
  15:         super.applyReferral(policy)
  16:     }
  17: }

 

The lossHistory property of policy is of type List[ LossHistory ]. The List class offers a number of handy methods that have functions as arguments. Simple methods such as filter take a function that takes an argument of T (same type argument passed to the List) and return a Boolean. The more complex fold functions (foldLeft and foldRight) are curried where the first parameter is a parameterised argument (lets call this type S for scalar type), and the second parameter is an argument of type (T, S) => S where type T is the type of the elements in the collection. The folding process takes the scalar value returned by the first function and applies the function in the second parameter set to that value and the first element of the list (in the case of foldLeft or the last for foldRight). The same process is recursively (note the Scala compiler has its own hand rolled tail call optimisation) applied to the rest of the list until there is no more list. So the above shorthand usage can be rewritten as shown below:

   1: val totalLoss = policy.lossHistory
   2:     .foldLeft( seed: BigDecimal )
   3:     ( ( accum: BigDecimal, losses: List[ LossHistory ] ) => accum + losses.amount )


The code can be compressed even more by using the obtuse looking "/:" method. I verbalise this by noting the resemblance of the colon to a fold and the direction of the slash slanting to the left. Note how the argument order is reversed because of the reverse ordering the compiler applies to methods ending in the ":" character. As you'd predict :\ or fold right uses the same ordering as the foldRight method so the order of the method args also matches the direction from which the folding process occurs. I leave it to the reader to decide if this is cool or confusing.

 

   1: val totalLoss = ( BigDecimal( 0 ) /: policy.lossHistory )( _ + _.amount )

 

The last line of the method is a call to super.applyReferral. Recall that calls to super are dynamically bound in traits. In the usage example below we'll see how an object is instantiated with a couple of traits. At that point we can now evaluate what the calls to super.applyReferral will do.

The next trait is InvalidStoragePeriodReferral

   1: package com.maslen.scala.policy.underwriting
   2:  
   3: import com.maslen.scala.policy.{Referral, CargoPolicy}
   4:  
   5: trait InvalidStoragePeriodReferral extends CargoAutoReferral {
   6:     val referrableStorageCode: String
   7:  
   8:     override def applyReferral(policy: CargoPolicy) {
   9:         if ( referrableStorageCode == policy.storagePeriod )
  10:             addReferral(new Referral("R002", "Invalid storage period"))
  11:  
  12:         super.applyReferral(policy)
  13:     }
  14: }

 

The only thing of note above is that Scala defines the == method to mean value equality in all cases. To get reference equality you use the eq method.

An example usage scenario is shown below:

   1: package com.maslen.scala.demo
   2:  
   3: import com.maslen.scala.policy.underwriting.{ExceededLossReferral,
   4: InvalidStoragePeriodReferral, CargoAutoReferral}
   5: import com.maslen.scala.policy.{LossHistory, CargoPolicy}
   6:  
   7: object SampleReferralUsage {
   8:  
   9:     def main(args: Array[ String ]) {
  10:         //
  11:         // Example 1. Scenario where single cargo polices include the
  12:         // InvalidStorage Period and ExceededLoss referral rules.
  13:         // Here we set up the product specific referral rules
  14:         //
  15:         val singleCargoAutoReferral = new CargoAutoReferral
  16:             with InvalidStoragePeriodReferral
  17:             with ExceededLossReferral {
  18:                 val exceededLoss = BigDecimal(10000)
  19:                 val referrableStorageCode = "1 year"
  20:             }
  21:  
  22:         // Setup the policy
  23:         val singleCargoPolicy = new CargoPolicy()
  24:         singleCargoPolicy.storagePeriod = "1 year"
  25:         singleCargoPolicy.lossHistory = LossHistory(2003, 15000) ::
  26:             singleCargoPolicy.lossHistory
  27:  
  28:         // Underwrite the policy
  29:         singleCargoAutoReferral.applyReferral(singleCargoPolicy)
  30:         println("Single Cargo Referrals")
  31:         println(singleCargoPolicy.referrals)
  32:  
  33:         //
  34:         // Example 2. Scenario where annual cargo polices only include the
  35:         // ExceededLoss referral rule
  36:         //
  37:         val annualCargoAutoReferral = new CargoAutoReferral
  38:             with ExceededLossReferral {
  39:             val exceededLoss = BigDecimal(10000)
  40:         }
  41:  
  42:         // Setup the policy
  43:         val annualCargoPolicy = new CargoPolicy()
  44:         annualCargoPolicy.lossHistory = LossHistory(2003, 15000) ::
  45:             annualCargoPolicy.lossHistory
  46:  
  47:         // Underwrite the policy
  48:         annualCargoAutoReferral.applyReferral(annualCargoPolicy)
  49:         println("Annual Cargo Referrals")
  50:         println(annualCargoPolicy.referrals)
  51:     }
  52: }

 

The example above shows a scenario where Cargo Policies are largely similar across single and annual variants.

Example one creates an anonymous object singleCargoAutoReferral value of type CargoAutoReferral with the InvalidStoragePeriodReferral and ExceededLossReferral traits mixed in. Recall the traits had abstract fields defined (not null fields) and so we initialise them here in the object declaration providing an excedededLoss limit of 10,000 and a referralStorageCode of "1 year".

We then create a test policy called singleCargoPolicy in a state that will trigger both referral rules.

Example two is similar except that it mixes in a single trait. The annualCargoPolicy is setup to trigger the one referral rule. The output is shown below

Single Cargo Referrals
List(Referral(R002,Invalid storage period), Referral(R001,Total loss limit exceeded))
Annual Cargo Referrals
List(Referral(R001,Total loss limit exceeded))

Running the demo on the CLR

The instructions for getting a CLR run-able version are here. The sample code only used Scala APIs so there is not requirement to alter any source code. I’ll be the first to admit this is not a straight forward process but here is how I got the code to .Net.

First you need to download the Scala.Net project. At present this means checking out the subversion source tree. I did this from a folder I created locally at c:\svncode\Scala.Net

svn co http://lampsvn.epfl.ch/svn-repos/scala/scala-experimental/trunk/bootstrap

Then you open a VS.Net command window and add the bin folder to the scala.Net compiler to the path variable:

set path=%path%;c:\svncode\scala.net\bin

Then you issue the rather long scala compiler command on a single line. I’ve broken the command here so you can read it. I’d like to think there was a way to specify a source folder and get the compiler to recursively look but I couldn’t find it.

scalacompiler -target:msil -d c:\svncode\ScalaBlogCode\clr-bin 
-Xassem-name SampleReferralUsage 
-Xshow-class com.maslen.scala.demo.SampleReferralUsage 
-Xassem-extdirs C:\svncode\scala.net\bin 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\demo\SampleReferralUsage.scala 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\policy\CargoPolicy.scala 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\policy\LossHistory.scala 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\policy\Policy.scala 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\policy\Referral.scala 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\policy\underwriting\AutoReferral.scala 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\policy\underwriting\CargoAutoReferral.scala 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\policy\underwriting\ExceededLossReferral.scala 
C:\svncode\ScalaBlogCode\src\com\maslen\scala\policy\underwriting\InvalidStoragePeriodReferral.scala

This will generate a text file with the msil code in it. From here you use the .Net supplied msil command from the target clr-bin folder specified above or you can fully path to it.

ilasm /exe SampleReferralUsage.msil

The last step is to copy all of the assemblies from the scala.Net bin folder to your clr-bin folder. Then you are up and running.

Conclusion

This post walked you through a few features of the Scala language and what it takes to get the code to run on the CLR. Having done a 2 year stint recently on Java, I can say Scala was a breath of fresh air compared to the very dated Java language that exists today. Unfortunately even on the JVM – in Australia at least – very few Scala jobs are out there.

If you work with both the JVM and CLR, Scala may be just what you need to support both platforms. If the advanced language features look interesting and you only code on the CLR, this language is worth keeping an eye on.

References

author: ChristianMaslen | Posted On Sunday, July 31, 2011 5:23 PM | Feedback (0)

My take on Windows 8 and what it means for developers


 

There’s been a lot of angst over what Windows 8 will mean to the developer experience. The fear is that history will repeat itself with developers compelled (at least those who want to target the new new hotness) to learn yet another technology stack.

So will you have to dump your Silverlight / WPF skills in favour of HTML5 / JavaScript? I doubt it. My guess is Microsoft will follow Google's lead and give developers the APIs they already know and offer a JavaScript based target. So in the case of the XAML technologies, you keep your mark-up and your C#, VB.Net or F# and one of your compilation targets is an HTML5 / JavaScript based target. That is a plug-in free Silverlight app. What would be cool in the case of a browser app is if the runtime could upon detecting an older browser offer the user the option of installing the Silverlight plug-in.

As I said. Just a guess and failing that there’s always WebSharper.

author: ChristianMaslen | Posted On Sunday, July 24, 2011 3:44 PM | Feedback (4)

Hello World


Technorati Tags: ,,,

Many language and API developers have their example of the “Hello World” program. Either to illustrate the simplest possible program that shows how to get something to run or to highlight a key concept in the language or API. For example how many Fibbonaci examples have you seen when being introduced to a functional language?

Calculating PI and Parallel.For

In the parallel programming world it looks like Hello World is the geometric approximation of PI. I took a look at the very interesting hands on lab on the subject. It outlines how to calculate PI using geometry and the Monte Carlo Method. It also shows how to progress from a single threaded version to a version using Parallel.For and how to analyse the code using the Visual Studio 2010 tools.

I tried this on a dual core PC and found the threaded version and the parallel.For version ran in roughly the same time. On my i7 with 8 hardware threads, I expected a bigger difference but not like this…

  Rough Execution Time
Single Threaded Version 37.1 seconds
Thread Pool Version 16.4 seconds
Parallel.For Version 74.5 seconds

I was disappointed to say the least. I can only conclude the extra hardware threads did more harm than good because of the need to synchronise access to the 2 collections in the sample.

Below is the improved version. This time we pre-calculate the random numbers so they don’t require the lock and we make use of a different overload of the Parallel.For method – one that allows you to keep running totals per thread so you don’t need to lock the total variable per loop iteration but instead lock it when each thread is done.

   1: using System;
   2: using System.Diagnostics;
   3: using System.Collections.Generic;
   4: using System.Threading;
   5: using System.Threading.Tasks;
   6:  
   7: namespace CalculatePI
   8: {
   9:     class Program
  10:     {
  11:         // Tuning constants:
  12:         // If you have lots of memory, increase NUMPOINTS to improve the accuracy
  13:         private const int NUMPOINTS = 10000000;
  14:         private const int RADIUS = 10000;
  15:  
  16:         // Value to seed the random number generator for each calculation.
  17:         // Using the same seed value ensures that the same results should be generated each time
  18:         private const int SEED = 269222;
  19:  
  20:         // If you have a very fast processor, increase SPINWAITS to show the effects of parallelization
  21:         private const int SPINWAITS = 1000;
  22:  
  23:         static double ParallelTasksPI()
  24:         {
  25:             Random random = new Random(SEED);
  26:             var points = new CartesianPoint[NUMPOINTS];
  27:             for (var i = 0; i < NUMPOINTS; i++)
  28:             {
  29:                 points[i] = new CartesianPoint(random.Next(RADIUS), random.Next(RADIUS));
  30:             }
  31:             int numPointsInCircle = 0;
  32:             Stopwatch timer = new Stopwatch();
  33:             timer.Start();
  34:  
  35:             try
  36:             {
  37:                 Parallel.For(0, NUMPOINTS,() => 0, (i, loopState, threadPointsInCircle) =>
  38:                     {
  39:                         double distanceFromOrigin =
  40:                             Math.Sqrt(points[i].XCoord * points[i].XCoord + points[i].YCoord * points[i].YCoord);
  41:                         
  42:                         if (distanceFromOrigin <= RADIUS)
  43:                             threadPointsInCircle++;
  44:  
  45:                         doAdditionalProcessing();
  46:                         return threadPointsInCircle;
  47:                     },
  48:                     (threadPointsInCircle) => Interlocked.Add(ref numPointsInCircle, threadPointsInCircle)
  49:                 );
  50:  
  51:                 double pi = 4.0 * numPointsInCircle / NUMPOINTS;
  52:                 return pi;
  53:             }
  54:             finally
  55:             {
  56:                 long milliseconds = timer.ElapsedMilliseconds;
  57:                 Console.WriteLine("ParallelTasksPI complete: Duration: {0} ms", milliseconds);
  58:                 Console.WriteLine("Points in pointsList: {0}. Points within circle: {1}", NUMPOINTS, numPointsInCircle);
  59:             }
  60:         }
  61:  
  62:         private static void doAdditionalProcessing()
  63:         {
  64:             Thread.SpinWait(SPINWAITS);
  65:         }
  66:  
  67:         static void Main(string[] args)
  68:         {
  69:             Console.WriteLine("Geometric approximation of PI calculated in parallel with TPL");
  70:             double pi = ParallelTasksPI();
  71:             Console.WriteLine("PI = {0}", pi);
  72:             Console.ReadLine();
  73:         }
  74:     }
  75:  
  76:     public class CartesianPoint
  77:     {
  78:         public CartesianPoint(int xCoord, int yCoord)
  79:         {
  80:             XCoord = xCoord;
  81:             YCoord = yCoord;
  82:         }
  83:  
  84:         public int XCoord { get; private set; }
  85:         public int YCoord { get; private set; }
  86:     }
  87: }

The run time now is 5.7 seconds. Much better. So as the functional guys say if you want your code to scale over multiple CPUs don’t share state.

Scaling to the GPU

While on the topic of PI calcs I thought I’d show the Accelerator version – one of a few APIs that offer a managed wrapper over GPU calls without being tied to any one GPU family. This one does it using Direct X so you don’t have to have a NVidia adapter as you’d need for CUDA. Unfortunately it’s still an incubator project with no immediate prospect of moving any time soon, but it’s worth showing the sample to illustrate how different GPU code is.

Here I’ve tried to preserve the calculation method from the lab but since GPU APIs don’t offer any way to make a thread wait (why would they?) we don’t simulate the extra work as before. As you can see things are done very differently indeed…

   1: using System;
   2: using System.Diagnostics;
   3: using System.Collections.Generic;
   4: using System.Threading;
   5: using System.Threading.Tasks;
   6:  
   7: using Microsoft.ParallelArrays;
   8: using A = Microsoft.ParallelArrays.ParallelArrays;
   9: using DPA = Microsoft.ParallelArrays.DoubleParallelArray;
  10: using IPA = Microsoft.ParallelArrays.IntParallelArray;
  11:  
  12: namespace CalculatePI
  13: {
  14:     class Program
  15:     {
  16:         // Tuning constants:
  17:         // If you have lots of memory, increase NUMPOINTS to improve the accuracy
  18:         private const int NUMPOINTS = 10000000;
  19:         private const int RADIUS = 10000;
  20:  
  21:         // Value to seed the random number generator for each calculation.
  22:         // Using the same seed value ensures that the same results should be generated each time
  23:         private const int SEED = 269222;
  24:  
  25:         static double AcceleratorPI()
  26:         {
  27:             Random random = new Random(SEED);
  28:             var xPoints = new double[NUMPOINTS];
  29:             var yPoints = new double[NUMPOINTS];
  30:             
  31:             Stopwatch timer = new Stopwatch();
  32:             timer.Start();
  33:  
  34:             for (var i = 0; i < NUMPOINTS; i++)
  35:             {
  36:                 xPoints[i] = random.Next(RADIUS);
  37:                 yPoints[i] = random.Next(RADIUS);
  38:             }
  39:             double numPointsInCircle = 0D;
  40:  
  41:             try
  42:             {
  43:                 var gpuXs = new DPA(xPoints);
  44:                 var gpuXsSquared = A.Multiply(gpuXs, gpuXs);
  45:  
  46:                 var gpuYs = new DPA(yPoints);
  47:                 var gpuYsSquared = A.Multiply(gpuYs, gpuYs);
  48:                 
  49:                 var gpuDistances = A.Sqrt(A.Add(gpuXsSquared, gpuYsSquared));
  50:                 var gpuCompares = A.Subtract(RADIUS, gpuDistances);
  51:                 
  52:                 var gpuZeros = new DPA(0D, gpuXs.Shape);
  53:                 var gpuOnes = new DPA(1D, gpuXs.Shape);
  54:                 
  55:                 var gpuPointsInCircle = A.Select(gpuCompares, gpuOnes, gpuZeros);
  56:                 var gpuNumPoints = A.Sum(gpuPointsInCircle);
  57:  
  58:                 // var targetDX = new DX9Target();
  59:                 // Not sure why DX9 throws a not supported exception on my PC
  60:                 var targetDX = new X64MulticoreTarget();
  61:                 numPointsInCircle = targetDX.ToArray1D(gpuNumPoints)[0];
  62:                 double pi = 4.0 * numPointsInCircle / NUMPOINTS;
  63:                 return pi;
  64:             }
  65:             finally
  66:             {
  67:                 long milliseconds = timer.ElapsedMilliseconds;
  68:                 Console.WriteLine("Accelerator PI complete: Duration: {0} ms", milliseconds);
  69:                 Console.WriteLine("Points in pointsList: {0}. Points within circle: {1}", 
  70:                     NUMPOINTS, numPointsInCircle);
  71:             }
  72:         }
  73:  
  74:         static void Main(string[] args)
  75:         {
  76:             Console.WriteLine("Geometric approximation of PI calculated in parallel with Accelerator");
  77:             double pi = AcceleratorPI();
  78:             Console.WriteLine("PI = {0}", pi);
  79:             Console.ReadLine();
  80:         }
  81:     }
  82: }

The code ran in about 0.7 of a second but as I mentioned I wasn’t able to put a wait loop in the code. Data is loaded to the GPUs using specialised arrays. I’ve aliased them to A and DPA for ParallelArrays and DoubleParallelArray respectively. Operations can only apply to the entire array and the arrays are immutable. I like to think of the parallel array operations in the same way as the functions in SQL Select statements. You can either apply a SQL function to a column to transform every row of an existing table or you can apply an aggregate to reduce the table rows to a group. This is why every part of the calculation is applied one bit at a time. For example I start with the gpuXs initialised with my random points on line 43 and then square them on the next line to produce a new array of the points squared. For a complex algorithm this can get tedious.

Conclusion

So to wrap things up if you want your code to scale to (many) multiple cores, minimise the shared state. If you want to take advantage of the extra power in most desktop PCs be prepared to make a lot of changes.

author: ChristianMaslen | Posted On Sunday, July 17, 2011 7:13 PM | Feedback (0)