C# – Optimizing performance with Parallel.For

By | 21/06/2023

In this post, we will see how to use Parallel.For to optimize the performance of our applications.
But first of all, what is Parallel.For?
“The Parallel For loop is part of the System.Threading.Tasks namespace introduced in .NET Framework 4.0. It provides a convenient way to divide a loop iteration into smaller, independent tasks that can be executed concurrently. By utilizing multiple threads, Parallel For can significantly speed up the execution of computationally intensive loops.”

Two important benefits of Parallel.For to highlight are:
IMPROVED PERFORMANCE:
The primary advantage of Parallel.For is improved performance. By distributing loop iterations across multiple threads and executing them in parallel, it leverages the processing power of modern multicore processors. This parallel execution can result in significantly faster completion times for computationally intensive tasks.
UTILIZING MULTICORE PROCESSORS:
With the prevalence of multicore processors, Parallel.For allows developers to fully utilize the available cores. By dividing the loop into smaller tasks and executing them concurrently, the application can achieve higher throughput and take advantage of the parallel processing capabilities of modern hardware.

However, before to decide to use Paralell.For, it’s important to consider the characteristics of our workload, the computational intensity of the operations, and the potential overhead associated with parallelism. Benchmarking can help us to determine whether the use of Parallel.For is beneficial or if a sequential approach is more suitable for your specific scenario.
In a nutshell, keep in mind that Parallel.For is not always the right choice!
Let’s see some example:


“SEQUENTIAL FOR” AS THE BEST CHOICE
In this example, the calculations for each element of the array are quick and not computationally intensive. The use of a Sequential For is more efficient of the Parallel.For because, it introduces additional overhead due to thread synchronization and management.

[BENCHMARKSEQUENTIALFOR.CS]

using BenchmarkDotNet.Attributes;

namespace TestParallel;

public class BenchmarkSequentialFor
{
    private const int N = 1000;
    private double[] inputArray;
    private double[] outputArray;

    [GlobalSetup]
    public void GlobalSetup()
    {
        inputArray = new double[N];
        outputArray = new double[N];

        // Initialize input array with random values
        Random random = new Random();
        for (int i = 0; i < N; i++)
        {
            inputArray[i] = random.NextDouble();
        }
    }

    [Benchmark]
    public void SequentialCalculation()
    {
        for (int i = 0; i < N; i++)
        {
            outputArray[i] = DoCalculation(inputArray[i]);
        }
    }

    [Benchmark]
    public void ParallelCalculation()
    {
        Parallel.For(0, N, i =>
        {
            outputArray[i] = DoCalculation(inputArray[i]);
        });
    }

    private double DoCalculation(double input)
    {
        // Simulate a simple calculation
        return input * 2;
    }
}

[PROGRAM.CS]

using BenchmarkDotNet.Running;
using TestParallel;

Console.WriteLine("Test SequentialFor");

BenchmarkRunner.Run<BenchmarkSequentialFor>();


If we run the application, the following will be the result:



“PARALLEL.FOR” AS THE BEST CHOICE
In this example, the calculations for each element of the array are independent and the use of Parallel.For allows for concurrent execution of the calculations across multiple threads. This parallel execution can lead to improved performance, especially when working with large arrays or performing computationally intensive tasks.

[BENCHMARKPARALLELFOR.CS]

using BenchmarkDotNet.Attributes;

namespace TestParallel;

public class BenchmarkParallelFor
{
    private const int N = 1000000;
    private double[] inputArray;
    private double[] outputArray;

    [GlobalSetup]
    public void GlobalSetup()
    {
        inputArray = new double[N];
        outputArray = new double[N];

        // Initialize input array with random values
        Random random = new Random();
        for (int i = 0; i < N; i++)
        {
            inputArray[i] = random.NextDouble();
        }
    }

    [Benchmark]
    public void SequentialCalculation()
    {
        for (int i = 0; i < N; i++)
        {
            outputArray[i] = DoCalculation(inputArray[i]);
        }
    }

    [Benchmark]
    public void ParallelCalculation()
    {
        Parallel.For(0, N, i =>
        {
            outputArray[i] = DoCalculation(inputArray[i]);
        });
    }

    private double DoCalculation(double input)
    {
        // Simulate a computationally intensive calculation
        return Math.Sin(input) * Math.Cos(input) + Math.Sqrt(input);
    }
}

[PROGRAM.CS]

using BenchmarkDotNet.Running;
using TestParallel;

Console.WriteLine("Test ParallelFor");

BenchmarkRunner.Run<BenchmarkParallelFor>();


If we run the application, the following will be the result:



Before to finish this post, I want to speak about Parallel.ForEach that can be more efficient in some situations. It is similar to Parallel.For but, it’s designed for working with collections, like lists or arrays.
It automatically splits the work among multiple processors or cores and processes each item in the collection independently.
The main difference between Parallel.For and Parallel.ForEach are:

  1. Parallel.For: It is a construct that allows parallel execution of a loop with a specified start and end index. It splits the loop iterations into smaller tasks that can be executed concurrently. We have control over the loop indices, and we can explicitly define the range of iterations to be processed in parallel.
  2. Parallel.ForEach: It is a construct that simplifies parallel processing of a collection by automatically partitioning the workload and distributing it among multiple threads. It handles the complexities of managing indices and load balancing for you. We iterate over each item in the collection without explicitly specifying loop indices.

Let’s see an example.

“PARALLEL.FOREACH” AS THE BEST CHOICE
In this example, where we want to process each item in a collection without explicit control over loop indices, Parallel.ForEach can be more suitable and convenient.
It automatically partitions the workload and distributes it among multiple threads, abstracting away the complexities of thread management and load balancing. This can result in cleaner code and easier parallel processing of collections.

[BENCHMARKPARALLELFOREACH.CS]

using BenchmarkDotNet.Attributes;

namespace TestParallel;

public class BenchmarkParallelForEach
{
    private const int N = 1000000;
    private List<int> numbers;
    private List<int> results;

    [GlobalSetup]
    public void GlobalSetup()
    {
        numbers = new List<int>(N);
        results = new List<int>(N);

        for (int i = 0; i < N; i++)
        {
            numbers.Add(i);
        }
    }

    [Benchmark]
    public void ParallelForProcess()
    {
        results.Clear();

        Parallel.For(0, N, i =>
        {
            int result = ProcessNumber(numbers[i]);
            //  I used lock in order to avoid potential race conditions
            lock (results)
            {
                results.Add(result);
            }
        });
    }

    [Benchmark]
    public void ParallelForEachProcess()
    {
        results.Clear();

        Parallel.ForEach(numbers, number =>
        {
            int result = ProcessNumber(number);
            // I used lock in order to avoid potential race conditions
            lock (results)
            {
                results.Add(result);
            }
        });
    }

    private int ProcessNumber(int number)
    {
        // Simulate some processing
        return number * 2;
    }
}

[PROGRAM.CS]

using BenchmarkDotNet.Running;
using TestParallel;

Console.WriteLine("Test ParallelForEach");

BenchmarkRunner.Run<BenchmarkParallelForEach>();


If we run the application, the following will be the result:



Category: C# Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *