Comparative Performance Evaluation of List-Based Age Summation Techniques in C#




Date Added (UTC):

07 May 2024 @ 13:50

Date Updated (UTC):

07 May 2024 @ 13:50


.NET Version(s):

.NET 8

Tag(s):

#Collections


Added By:
Profile Image

Blog   
Wilmington, DE 19808, USA    
A dedicated executive technical architect who is focused on expanding organizations technology capabilities.

Benchmark Results:





Benchmark Code:



using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<PersonAgeSumPerformanceBenchmark>();

[MemoryDiagnoser]
public class PersonAgeSumPerformanceBenchmark
{
    private List<Person> _peopleList = default!;
    private const int RandomSeed = 12345;

    [Params(10, 1_000, 10_000, 100_000)]
    public int NumberOfPeople { get; set; }

    public class Person
    {
        public int Age { get; set; }

        public Person(int age)
        {
            Age = age;
        }
    }

    [GlobalSetup]
    public void SetupPeopleList()
    {
        var randomGenerator = new Random(RandomSeed);
        _peopleList = new List<Person>(NumberOfPeople);
        for (int index = 0; index < NumberOfPeople; index++)
        {
            _peopleList.Add(new Person(randomGenerator.Next(1, 100)));
        }
    }

    [Benchmark(Baseline = true)]
    public int SumAgesUsingForeachLoop()
    {
        int totalAgeSum = 0;
        foreach (Person person in _peopleList)
        {
            totalAgeSum += person.Age;
        }
        return totalAgeSum;
    }

    [Benchmark]
    public int SumAgesUsingListForEachMethod()
    {
        var totalAgeSum = 0;
        _peopleList.ForEach(person => IncrementAgeSum(ref totalAgeSum, person.Age));
        return totalAgeSum;

        static void IncrementAgeSum(ref int sum, int ageToAdd)
        {
            sum += ageToAdd;
        }
    }

    [Benchmark]
    public int SumAgesUsingForLoop()
    {
        var totalAgeSum = 0;
        for (int index = 0; index < _peopleList.Count; index++)
        {
            totalAgeSum += _peopleList[index].Age;
        }
        return totalAgeSum;
    }

    [Benchmark]
    public int SumAgesUsingWhileLoop()
    {
        int totalAgeSum = 0;
        int index = 0;
        while (index < _peopleList.Count)
        {
            totalAgeSum += _peopleList[index].Age;
            index++;
        }
        return totalAgeSum;
    }

    [Benchmark]
    public int SumAgesUsingDoWhileLoop()
    {
        if (_peopleList.Count == 0) return 0;
        var totalAgeSum = 0;
        var index = 0;
        do
        {
            totalAgeSum += _peopleList[index].Age;
            index++;
        } while (index < _peopleList.Count);

        return totalAgeSum;
    }

    [Benchmark]
    public int SumAgesUsingSpanForeachLoop()
    {
        var totalAgeSum = 0;
        foreach (var person in CollectionsMarshal.AsSpan(_peopleList))
        {
            totalAgeSum += person.Age;
        }
        return totalAgeSum;
    }

    [Benchmark]
    public int SumAgesUsingParallelFor()
    {
        var totalAgeSum = 0;
        Parallel.ForEach(_peopleList, 
            () => 0,   
            (person, loopState, localAgeSum) => localAgeSum + person.Age,
            localAgeSum => Interlocked.Add(ref totalAgeSum, localAgeSum));

        return totalAgeSum;
    }
}

// .NET 8 Lowered C# Code unavailable due to errors:
error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
error CS0103: The name 'Interlocked' does not exist in the current context

// .NET 8 IL Code unavailable due to errors:
error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
error CS0103: The name 'Interlocked' does not exist in the current context

// .NET 8 Jit Asm Code unavailable due to errors:
error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
error CS0103: The name 'Interlocked' does not exist in the current context


Benchmark Description:


# PersonAgeSumPerformanceBenchmark Class Overview ## Class Structure and Purpose The `PersonAgeSumPerformanceBenchmark` class is designed to evaluate the performance of different methods for summing ages in a list of `Person` objects. It utilizes the `BenchmarkDotNet` library, which is tailored for performance benchmarking in .NET. ### Key Components - **Benchmark Setup**: Incorporates `BenchmarkDotNet` attributes to define and control benchmarks. - **Data Initialization**: Initializes a list of `Person` objects with randomly generated ages. Varies the list size with `NumberOfPeople` to test scaling. - **MemoryDiagnoser Attribute**: Collects memory allocation statistics for each benchmark. ## Benchmark Methods Each method tests a different approach to sum the ages of all persons in the list: 1. **SumAgesUsingForeachLoop**: Uses a `foreach` loop. 2. **SumAgesUsingListForEachMethod**: Utilizes `List<T>.ForEach` with a delegate. 3. **SumAgesUsingForLoop**: Employs a `for` loop by index. 4. **SumAgesUsingWhileLoop**: Uses a `while` loop. 5. **SumAgesUsingDoWhileLoop**: Uses a `do-while` loop. 6. **SumAgesUsingSpanForeachLoop**: Uses a `Span<T>` in a `foreach` loop. 7. **SumAgesUsingParallelFor**: Implements parallel processing with `Parallel.ForEach`. ## Goals of the Benchmarks - **Performance Analysis**: Measures execution speed and efficiency of each method. - **Scalability Testing**: Evaluates how methods perform as the number of people increases. - **Resource Usage Evaluation**: Assesses memory usage, crucial for applications managing large datasets. The benchmarks aim to provide metrics to help developers choose the most effective method for their applications based on performance, scalability, and efficiency needs.

This benchmark suite is designed to measure the performance of different methods for summing the ages of a list of `Person` objects in C#. The setup involves creating a list of `Person` objects with ages generated randomly. The size of this list varies based on the parameter `NumberOfPeople`, which takes on values 10, 1,000, 10,000, and 100,000 to test the performance across different scales. The benchmarks are run using BenchmarkDotNet, a powerful library for benchmarking .NET code, though the exact .NET version isn't specified in the provided code snippet. ### General Setup - **Framework:** The benchmarks are likely run on a recent .NET version, considering the use of `BenchmarkDotNet` and features like `CollectionsMarshal.AsSpan`, suggesting at least .NET Core 3.1 or .NET 5/6. - **Configuration:** The `MemoryDiagnoser` attribute is used to diagnose memory allocations, indicating that memory efficiency is also under consideration. - **Data Setup:** A list of `Person` objects is populated in the `GlobalSetup` method, ensuring that each benchmark method operates on the same dataset for consistency. ### Benchmark Methods and Rationale #### 1. `SumAgesUsingForeachLoop` - **Purpose:** Measures the performance of a simple `foreach` loop iterating over a list. - **Importance:** This method tests the baseline performance of iterating with a `foreach` loop, which is straightforward and commonly used. - **Expected Insights:** It provides a baseline for comparing the efficiency of other looping constructs or methods for summing values. #### 2. `SumAgesUsingListForEachMethod` - **Purpose:** Evaluates the performance of the `List<T>.ForEach` method with a lambda expression. - **Importance:** It tests the overhead of lambda expressions and the `List<T>.ForEach` method compared to simple loops. - **Expected Insights:** This method might show additional overhead due to delegate invocation, which could be less efficient than simple loops. #### 3. `SumAgesUsingForLoop` - **Purpose:** Measures the performance of a traditional `for` loop accessing list elements by index. - **Importance:** Provides insight into the efficiency of index-based list access and loop performance. - **Expected Insights:** Often, `for` loops are more performant than `foreach` for list access, due to reduced overhead. #### 4. `SumAgesUsingWhileLoop` - **Purpose:** Tests the performance of a `while` loop for iterating over the list. - **Importance:** Similar to the `for` loop but uses a different looping construct, providing comparative insights. - **Expected Insights:** Performance might be similar to the `for` loop, depending on compiler optimizations. #### 5. `SumAgesUsingDoWhileLoop` - **Purpose:** Evaluates how a `do-while` loop performs for the same task. - **Importance:** Ensures that the loop body is executed at least once, testing performance in a slightly different control flow context. - **Expected Insights:** Might be slightly less efficient for empty lists due to the initial check but similar to `while` loops for non-empty lists. #### 6. `SumAgesUsingSpanForeachLoop` - **Purpose:** Measures performance using `Span<T>` with a `foreach` loop, leveraging stack-allocated memory for the iteration. - **Importance:** Tests the benefits of `Span<T>` in reducing heap allocations and possibly improving cache locality. - **Expected Insights:** Could offer significant performance improvements, especially for larger datasets, by minimizing heap allocations. #### 7. `SumAgesUsingParallelFor` - **Purpose:** Evaluates the performance of parallelizing the sum operation using `Parallel.ForEach`. - **Importance:** Important for understanding the scalability of the operation across multiple CPU cores. - **Expected Insights:** Expected to show significant performance improvements on multi-core systems, especially for large datasets, but might introduce overhead for smaller datasets. ### Conclusion Each benchmark method is designed to test a specific aspect of iterating and summing values in a list, from simple loop constructs to more advanced parallel processing techniques. The results from these benchmarks can provide valuable insights into choosing the most efficient method for similar operations, considering both execution time and memory allocations. Understanding the trade-offs between these methods is crucial for writing high-performance C# code, especially in scenarios where processing large datasets or requiring high throughput.


Benchmark Comments: