วันอาทิตย์ที่ 23 กันยายน พ.ศ. 2555

Refactorings for simplifying of .NET 4.0 parallel computing development

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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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;
}
}

And the standard function that returns the list of primes in the specified limit:
01
02
03
04
05
06
07
08
09
10
11
12
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;
}

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:
1
2
3
4
5
6
7
8
static void Main(string[] args)
{
Stopwatch stopwatch = Stopwatch.StartNew();
Find(300000);
stopwatch.Stop();
Console.WriteLine("Time passed: " +
stopwatch.ElapsedMilliseconds + " ms.");
}

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:
1
2
3
4
5
6
7
static List FindLINQ(int limit)
{
IEnumerable numbers = Enumerable.Range(2, limit - 1);
return (from n in numbers
where Prime.IsPrime(n)
select n).ToList();
}

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:
1
2
3
4
5
6
7
8
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);
}
}

The standard code will look like this:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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;
}

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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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;
}

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.

ไม่มีความคิดเห็น:

แสดงความคิดเห็น