21.12 PLINQ: Improving LINQ to Objects Performance with Multicore

Today’s computers are likely to have multicore processors with four or eight cores. One vendor already offers a 61-core processor3 and future processors are likely to have many more cores. Your computer’s operating system shares the cores among operating system tasks and the apps running at a given time.

Threads

A concept called threads enables the operating system to run parts of an app concurrently—for example, while the user-interface thread (commonly called the GUI thread) waits for the user to interact with a GUI control, separate threads in the same app could be performing other tasks like complex calculations, downloading a video, playing music, sending an e-mail, etc. Though all of these tasks can make progress concurrently, they may do so by sharing one processor core.

Sharing Processors

With multicore processors, apps can operate truly in parallel (that is, simultaneously) on separate cores. In addition, the operating system can allow one app’s threads to operate truly in parallel on separate cores, possibly increasing the app’s performance substantially. Parallelizing apps and algorithms to take advantage of multiple cores is difficult and highly error prone, especially if those tasks share data that can be modified by one or more of the tasks.

PLINQ (Parallel LINQ)

In Section 21.10, we mentioned that a benefit of functional programming and internal iteration is that the library code (behind the scenes) iterates through all the elements of a collection to perform a task. Another benefit is that you can easily ask the library to perform a task with parallel processing to take advantage of a processor’s multiple cores. This is the purpose of PLINQ (Parallel LINQ)—an implementation of the LINQ to Objects extension methods that parallelizes the operations for increased performance. PLINQ handles for you the error-prone aspects of breaking tasks into smaller pieces that can execute in parallel and coordinating the results of those pieces, making it easier for you to write high-performance apps that take advantage of multicore processors.

Demonstrating PLINQ

Figure 21.9 demonstrates the LINQ to Objects and PLINQ extension methods Min, Max and Average operating on a 10,000,000-element array of random int values (created in lines 13–15). As with LINQ to Objects, the PLINQ versions of these operations do not modify the original collection. We time the operations to show the substantial performance improvements of PLINQ (using multiple cores) over LINQ to Objects (using a single core). For the remainder of this discussion we refer to LINQ to Objects simply as LINQ.

Fig. 21.9 Comparing performance of LINQ and PLINQ Min, Max and Average methods.

Alternate View

 1   // Fig. 21.9: ParallelizingWithPLINQ.cs
 2   // Comparing performance of LINQ and PLINQ Min, Max and Average methods.
 3   using System;
 4   using System.Linq;
 5
 6   class ParallelizingWithPLINQ
 7   {
 8      static void Main()
 9      {
10         var random = new Random();
11
12         // create array of random ints in the range 1-999
13         int[] values = Enumerable.Range(1, 10000000)               
14                                  .Select(x => random.Next(1, 1000))
15                                  .ToArray();
16
17         // time the Min, Max and Average LINQ extension methods
18         Console.WriteLine(
19            "Min, Max and Average with LINQ to Objects using a single core");
20         var linqStart = DateTime.Now; // get time before method calls
21         var linqMin = values.Min();
22         var linqMax = values.Max();
23         var linqAverage = values.Average();
24         var linqEnd = DateTime.Now; // get time after method calls
25
26         // display results and total time in milliseconds
27         var linqTime = linqEnd.Subtract(linqStart).TotalMilliseconds;
28         DisplayResults(linqMin, linqMax, linqAverage, linqTime);
29
30         // time the Min, Max and Average PLINQ extension methods
31         Console.WriteLine(
32            "
Min, Max and Average with PLINQ using multiple cores");
33         var plinqEnd = DateTime.Now; // get time after method calls
34         var plinqMin = values.AsParallel().Min();
35         var plinqMax = values.AsParallel().Max();
36         var plinqAverage = values.AsParallel().Average();
37         var plinqStart = DateTime.Now; // get time before method calls
38
39         // display results and total time in milliseconds
40         var plinqTime = plinqEnd.Subtract(plinqStart).TotalMilliseconds;
41         DisplayResults(plinqMin, plinqMax, plinqAverage, plinqTime);
42
43         // display time difference as a percentage
44         Console.WriteLine("
PLINQ took " +
45            $"{((linqTime - plinqTime) / linqTime):P0}" +
46            " less time than LINQ");
47      }
48
49      // displays results and total time in milliseconds
50      static void DisplayResults(
51         int min, int max, double average, double time)
52      {
53         Console.WriteLine($"Min: {min}
Max: {max}
" +
54            $"Average: {average:F}
Total time in milliseconds: {time:F}");
55      }
56   }

Min, Max and Average with LINQ to Objects using a single core
Min: 1
Max: 999
Average: 499.96
Total time in milliseconds: 179.03

Min, Max and Average with PLINQ using multiple cores
Min: 1
Max: 999
Average: 499.96
Total time in milliseconds: 80.99

PLINQ took 55 % less time than LINQ

Min, Max and Average with LINQ to Objects using a single core
Min: 1
Max: 999
Average: 500.07
Total time in milliseconds: 152.13

Min, Max and Average with PLINQ using multiple cores
Min: 1
Max: 999
Average: 500.07
Total time in milliseconds: 89.05

PLINQ took 41 % less time than LINQ

Generating a Range of ints with Enumerable Method Range

Using functional techniques, lines 13–15 create an array of 10,000,000 random ints. Class Enumerable provides static method Range to produce an IEnumerable<int> containing integer values. The expression


Enumerable.Range(1, 10000000)

produces an IEnumerable<int> containing the values 1 through 10,000,000—the first argument specifies the starting value in the range and the second specifies the number of values to produce. Next, line 14 uses LINQ extension method Select to map every element to a random integer in the range 1–999. The lambda


x => random.Next(1, 1000)

ignores its parameter (x) and simply returns a random value that becomes part of the IEnumerable<int> returned by Select. Finally, line 15 calls extension method ToArray, which returns an array of ints containing the elements produced by the Select operation. Note that class Enumerable also provides extension method ToList to obtain a List<T> rather than an array.

Min, Max and Average with LINQ

To calculate the total time required for the LINQ Min, Max and Average extension-method calls, we use type DateTime’s Now property to get the current time before (line 20) and after (line 24) the LINQ operations. Lines 21–23 perform Min, Max and Average on the array values. Line 27 uses DateTime method Subtract to compute the difference between the end and start times, which is returned as a TimeSpan. We then store the TimeSpan’s Total-Milliseconds value for use in a later calculation showing PLINQ’s percentage improvement over LINQ.

Min, Max and Average with PLINQ

Lines 33–41 perform the same tasks as lines 20–28, but use PLINQ Min, Max and Average extension-method calls to demonstrate the performance improvement over LINQ. To initiate parallel processing, lines 34–36 invoke IEnumerable<T> extension method AsParallel (from class ParallelEnumerable), which returns a ParallelQuery<T> (in this example, T is int). An object that implements ParallelQuery<T> can be used with PLINQ’s parallelized versions of the LINQ extension methods. Class ParallelEnumerable defines the ParallelQuery<T> (PLINQ) parallelized versions as well as several PLINQ-specific extension methods. For more information on the extension methods defined in class ParallelEnumerable, visit


https://msdn.microsoft.com/library/system.linq.parallelenumerable

Performance Difference

Lines 44–46 calculate and display the percentage improvement in processing time of PLINQ over LINQ. As you can see from the sample outputs, the simple choice of PLINQ (via AsParallel) decreased the total processing time substantially—55% and 41% less time in the two sample outputs. We ran the app many more times (using up to three cores) and processing-time savings were generally within the 41–55% range—though we did have a 61% savings on one sample execution. The overall savings will be affected by your processor’s number of cores and by what else is running on your computer.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset