English
Reduction, sorting, and performance
Parallel reduction and prefix sum
Reduction combines a dataset into one value using an associative operation
Figure 6.5. Parallel reduction tree
Total work (operation count) remains localInit/localFinally and Aggregate work.
A prefix sum (scan) computes every intermediate result: for s[i] = s[i - 1] + x[i] has a dependency between iterations, but the problem can be parallelized using the Blelloch scan for an array of length
- Up-sweep builds a reduction tree in place: at the level with stride
, executea[i + s - 1] += a[i + s/2 - 1]for each pair. After the up-sweep, the last element contains the entire array’s sum. - Down-sweep replaces the last element with the identity element (0), then, at levels with stride
, gives each pair’s left element the right element’s (parent’s) value, and the right element the sum of the parent and the old left value.
After the down-sweep, the array contains the exclusive prefix sum. The algorithm performs about
Parallel sorting algorithms
Sorting is a classic example combining data and task parallelism.
Merge sort
Merge sort splits an array in half, sorts the halves, and merges them. The halves are independent, so they can be sorted in parallel, for example with Parallel.Invoke (Fig. 6.6). Two details matter:
- cutoff: sort fragments below a threshold (thousands to tens of thousands of elements) sequentially, because creating tasks for small fragments costs more than the work itself;
- depth limit: create new tasks only at the top
recursion levels; beyond that, recurse sequentially.
Figure 6.6. Parallel merge sort
The final merge runs on one thread and scans the entire array, so Amdahl’s law (Topic 1) limits speedup: the “Parallel merge sort” example achieves only 4–6 on 16 logical processors. For greater speedup, parallelize merging too: locate the middle element of one half in the other half using binary search, then merge the two parts independently.
Quicksort is parallelized similarly: partitioning around the pivot is sequential, while tasks sort the two parts in parallel with the same cutoff and depth limit. The parts may be very unequal, so scalability is worse.
Odd–even transposition sort
Odd–even transposition sort is a parallel version of bubble sort. An array of
Sample sort
Sample sort scales best with many processors and on clusters (Topic 12): select and sort a random sample from the array; choose
Other data parallelism patterns
- Parallel search.
Parallel.ForwithStop()finds any matching element; withBreak(), the first. PLINQ providesAnyandFirst(withAsOrdered). If the target element is near the beginning, sequential search may be faster. - Histogram. Use a local histogram for each partition (
localInit) and merge inlocalFinally; a shared array withInterlocked.Incrementon each element is slow because threads constantly compete for the same counters and cache lines (Topic 4). - Image filter. Process rows independently and write results to a new array (not in place): the filter reads neighboring pixels that another thread might already have changed.
- Monte Carlo method. A
Randominstance is not thread-safe: concurrent calls from multiple threads can corrupt its state and cause it to return zeros.Random.Sharedis thread-safe, but results are not reproducible. For reproducible results, split work into a fixed number of blocks, with blockkcreating its ownnew Random(seed + k): the result is independent of thread count (lab, Example 2). - Nested loops. Usually parallelize the outer loop: each iteration does more work, with less overhead.
Performance analysis
For a parallel program, measure time
Strong scaling keeps problem size fixed while increasing
Screenshot
Windows Terminal: dotnet run -c Release -- 1,2,4,8,16 in the merge sort project; the aligned table n, p, time, S, E and the line with speedup.csv
Figure 6.7. Merge sort speedup and efficiency table
Open the CSV in Excel (Data → From Text/CSV) or LibreOffice Calc (comma delimiter, period decimal separator), then plot
- the sequential portion (Amdahl’s law): reading data, the final merge, output;
- task and partitioning overhead when work per iteration is small (granularity);
- memory limitations: when computation is limited by memory bandwidth rather than cores, speedup may stop after just a few threads;
- SMT/Hyper-Threading: the i9-11900KF has 16 logical processors but only 8 physical cores, so going from 8 to 16 threads yields a much smaller gain;
- garbage collection: code allocating many objects (strings, tuples) waits for the garbage collector. Server GC (
<ServerGarbageCollection>true</ServerGarbageCollection>in the project file) made theGroupByversion of the “Text analysis” example almost twice as fast; - false sharing (Topic 4) and uneven partition workloads.
Use a profiler to see where threads sit idle. In JetBrains Rider, dotTrace’s Timeline mode shows each thread’s activity on a timeline https://www.jetbrains.com/help/rider/Profiling_Applications.html: choose Run → Switch Profiling Configuration → Timeline, then Run → Profile … (Fig. 6.8). Solid worker thread bars indicate computation; gaps indicate waiting or blocking.
Screenshot
Rider: Run → Switch Profiling Configuration → Timeline; profile the merge sort example with program argument 16; Get Snapshot; thread lanes of .NET ThreadPool workers, filter by method ParallelMergeSort.Sort
Figure 6.8. Parallel sorting threads in dotTrace (Timeline)