Home > Overview, Refactorings > Refactorings for simplifying of .NET 4.0 parallel computing development

Refactorings for simplifying of .NET 4.0 parallel computing development

May 18th, 2011

Many personal computers and workstations have two or four cores that enable multiple threads to be executed simultaneously. .Net Framework ver. 4.0 has been introduced a standardized and simplified way for creating robust, scalable and reliable multi-threaded applications. The parallel programming extension of .NET 4.0 allows the developer to create applications that exploit the power of multi-core and multi-processor computers. The flexible thread APIs of the extension are much simpler to use and more powerful than standard .NET threads.

The extension implements the concept of automatic dynamic parallelization of applications. It provides both ease-of-use and scalability in development of parallel programs. The concept is naturally integrated into .NET Framework by means of templatized classes (introduced in C# with generics) that encapsulate all low-level details such as threading, synchronization, scheduling, load balancing, etc., which makes the extension a powerful tool for implementing high-performance parallel applications.

Refactor! Pro provides several parallel computing refactorings that can help you to parallelize your code to distribute work across multiple processors. Here they are:

  • Convert to Parallel

Converts the code to run in parallel.

  • Execute Statements in Parallel

Executes the selected independent statements in parallel.

  • Execute Statements in Serial

Moves the child statements out from the Parallel.Invoke call and executes them serially.

Passes the appropriate BeginXXX and EndXXX methods (corresponding to this statement) to the FromAsync method of Task.Factory, launching an asynchronous operation and returning a handle to the associated Task.

Passes the current statement to the StartNew method of Task.Factory, launching an asynchronous operation and returning a handle to the associated Task.

All refactorings work in both CSharp and Visual Basic languages. In this article we will review the first three refactorings, and see how they help to speed-up the code in a real sample. There are several parallel programming methods we are going to use:

  • Parallel.For
  • AsParallel
  • Parallel.Invoke
  • and a standard serial calculation method to compare with.

Consider that we have the function that determines whether the specified number is prime:

public class Prime
{
  /// <summary>
  /// Determines whether the specified number is prime.
  /// </summary>
  /// <param name="number">The number.</param>
  /// <returns>
  /// true if the specified number is prime; otherwise, false.
  /// </returns>
  public static bool IsPrime(int number)
  {
    for (int d = 2; d <= number / 2; d++)
    {
       if (number % d == 0)
         return false;
    }
    return number > 1;
  }
}

Show Visual Basic code… »

Public Class Prime
  ''' <summary>
  ''' Determines whether the specified number is prime.
  ''' </summary>
  ''' <param name="number">The number.</param>
  ''' <returns>
  ''' true if the specified number is prime; otherwise, false.
  ''' </returns>
  Public Shared Function IsPrime(ByVal number As Integer) As Boolean
    For d As Integer = 2 To number / 2
      If number Mod d = 0 Then
        Return False
      End If
    Next d
    Return number > 1
  End Function
End Class

And the standard function that returns the list of primes in the specified limit:

static List Find(int limit)
{
  var result = new List();

  for (int i = 2; i < limit; i++)
  {
    if (Prime.IsPrime(i))
      result.Add(i);
  }

  return result;
}

Show Visual Basic code… »

Shared Function Find(ByVal limit As Integer) As List(Of Integer)
  Dim result = New List(Of Integer)()

  For i As Integer = 2 To limit
    If Prime.IsPrime(i) Then
      result.Add(i)
    End If
  Next i

  Return result
End Function

Using the System.Diagnostics.Stopwatch class, we will count how much time it takes for finding all primes within the limit. The limit will be 300000:

static void Main(string[] args)
{
  Stopwatch stopwatch = Stopwatch.StartNew();
  Find(300000);
  stopwatch.Stop();
  Console.WriteLine("Time passed: " +
                            stopwatch.ElapsedMilliseconds + " ms.");
}

Show Visual Basic code… »

Shared Sub Main()
  Dim stopwatch As Stopwatch = Stopwatch.StartNew()
  Find(300000)
  stopwatch.Stop()
  Console.WriteLine("Time passed: " + _
                            stopwatch.ElapsedMilliseconds + " ms.")
End Sub

The result of the standard code run is 18328 ms. Average CPU usage is 52%. Here’s the CPU usage history of the Intel(R) Core(TM)2 DUO CPU:

Refactor! CPU usage history #1

Now, let’s use the Convert to Parallel refactoring and improve the code:

Refactor! Convert to Parallel preview - Parallel.For

The result of the Parallel.For code run is 9727 ms. Average CPU usage is 100%. CPU usage history:

Refactor! CPU usage history #2

Let’s change the Find method to use LINQ instead:

static List FindLINQ(int limit)
{
  IEnumerable numbers = Enumerable.Range(2, limit - 1);
  return (from n in numbers
          where Prime.IsPrime(n)
          select n).ToList();
}

Show Visual Basic code… »

Shared Function FindLINQ(ByVal limit As Integer) As List(Of Integer)
  Dim numbers As IEnumerable(Of Integer) = Enumerable.Range(2, limit - 1)
  Return (From n In numbers _
            Where Prime.IsPrime(n) _
            Select n).ToList()
End Function

Result time: 19555 ms. Average CPU usage: 100%. CPU usage history:

Refactor! CPU usage history #3

Once again, using the same Convert to Parallel refactoring change the code:

Refactor! Convert to Parallel preview - AsParallel

Result time: 10111 ms. Average CPU usage: 100%. CPU usage history:

Refactor! CPU usage history #4

Now let’s use the Parallel.Invoke method. To use this method, we’ll split the limited number into 10 parts and make calculation in parallel. An additional helper method is needed in this case:

static void CheckRange(List result, int n, int limit, int factor)
{
  for (int i = n * (limit / factor); i < (n + 1) * (limit / factor); i++)
  {
    if (Prime.IsPrime(i))
      result.Add(i);
  }
}

Show Visual Basic code… »

Shared Sub CheckRange(ByVal result As List(Of Integer), ByVal n As Integer, ByVal limit As Integer, ByVal factor As Integer)
  For i As Integer = n * (limit / factor) To (n + 1) * (limit / factor)
    If Prime.IsPrime(i) Then
      result.Add(i)
    End If
  Next i
End Sub

The standard code will look like this:

static List FindRange(int limit)
{
  var result = new List();
  CheckRange(result, 0, limit, 10);
  CheckRange(result, 1, limit, 10);
  CheckRange(result, 2, limit, 10);
  CheckRange(result, 3, limit, 10);
  CheckRange(result, 4, limit, 10);
  CheckRange(result, 5, limit, 10);
  CheckRange(result, 6, limit, 10);
  CheckRange(result, 7, limit, 10);
  CheckRange(result, 8, limit, 10);
  CheckRange(result, 9, limit, 10);
  return result;
}

Show Visual Basic code… »

Shared Function FindRange(ByVal limit As Integer) As List(Of Integer)
  Dim result = New List(Of Integer)()
  CheckRange(result, 0, limit, 10)
  CheckRange(result, 1, limit, 10)
  CheckRange(result, 2, limit, 10)
  CheckRange(result, 3, limit, 10)
  CheckRange(result, 4, limit, 10)
  CheckRange(result, 5, limit, 10)
  CheckRange(result, 6, limit, 10)
  CheckRange(result, 7, limit, 10)
  CheckRange(result, 8, limit, 10)
  CheckRange(result, 9, limit, 10)
  Return result
End Function

Result time without parallel optimization: 17816 ms. CPU usage: 52%. CPU usage history:

Refactor! CPU usage history #5

Now, use the Execute Statements in Parallel refactoring: The Parallel.Invoke method takes an array of delegates as an argument. The new code will look like this:

static List FindRangeParallel(int limit)
{
  var result = new List();
  Parallel.Invoke(
    () => CheckRange(result, 0, limit, 10),
    () => CheckRange(result, 1, limit, 10),
    () => CheckRange(result, 2, limit, 10),
    () => CheckRange(result, 3, limit, 10),
    () => CheckRange(result, 4, limit, 10),
    () => CheckRange(result, 5, limit, 10),
    () => CheckRange(result, 6, limit, 10),
    () => CheckRange(result, 7, limit, 10),
    () => CheckRange(result, 8, limit, 10),
    () => CheckRange(result, 9, limit, 10));
  return result;
}

Show Visual Basic code… »

Shared Function FindRangeParallel(ByVal limit As Integer) As List(Of Integer)
  Dim result = New List(Of Integer)()
  Parallel.Invoke(
    Sub() CheckRange(result, 0, limit, 10),
    Sub() CheckRange(result, 1, limit, 10),
    Sub() CheckRange(result, 2, limit, 10),
    Sub() CheckRange(result, 3, limit, 10),
    Sub() CheckRange(result, 4, limit, 10),
    Sub() CheckRange(result, 5, limit, 10),
    Sub() CheckRange(result, 6, limit, 10),
    Sub() CheckRange(result, 7, limit, 10),
    Sub() CheckRange(result, 8, limit, 10),
    Sub() CheckRange(result, 9, limit, 10))
  Return result
End Function

Result time: 10530 ms. CPU usage: 100%. CPU usage history:

Refactor! CPU usage history #6

The Execute Statements in Serial refactoring is an the opposite of the Execute Statements in Parallel, which moves all code from the Parallel.Invoke call and executes it as usual (serially).

Here’s the comparison chart of the different parallel methods used in comparison to the standard serial methods:

Refactor! Parallel computing graph

Time chart:

Refactor! Parallel computing time graph

As you can see, using parallel computing almost doubles the speed of the code, even when the number of calculations is small. Parallel refactorings from Refactor! Pro make it easy to improve the speed of your code.

—–
Products: Refactor! Pro
Versions: 12.1 and up
VS IDEs: any
Updated: Sep/17/2012
ID: R026

Similar Posts:

  1. No comments yet. Be the first and leave a comment!