Code Snippets for the List<T> Analysis Post

This post contains the code referenced by the Boxing Performance in C# – Versus Generics article, the second in a 3-article set that discusses boxing, ArrayList and List<T>.

IndividualListBenchmark

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Configs;


namespace IndividualListBenchmark
{
    [Config(typeof(Config))]
    [MemoryDiagnoser]
    [DisassemblyDiagnoser(printAsm: true, printSource: true)]
    public class UnderTest
    {
        
        // Used only to force 1 op/iteration in order to keep GC away as much as possible
        private class Config : ManualConfig
        {
            // Force just one iteration, with 1 ops/iteration
            public Config()
            {
                Add(Job.Default.WithInvocationCount(1).WithUnrollFactor(1).WithIterationCount(20));
            }
        }

        const int noNumbers = 10000000;
        Random random = new Random(1);

        [Benchmark]
        public void GenerateRandomNumbersAndAddToListOfInt()
        {
            List<int> numbers = new List<int>();
            for (int i = 0; i < noNumbers; i++)
            {
                int currentNumber = random.Next(10);
                numbers.Add(currentNumber);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<UnderTest>();
        }
    }
}

AddStringsToList

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AddRandomNumbersToList
{
    class Program
    {
        static void Main(string[] args)
        {
            const int noNumbers = 10000000;

            List<string> numbers = new List<string>();
            Random random = new Random(1);  // use the same seed as to make
                                            //  benchmarking consistent

            for (int i = 0; i < noNumbers; i++)
            {
                int currentNumber = random.Next(10);  // generate a non-negative
                                                      //  random number less than 10
                string currentString = currentNumber.ToString();
                numbers.Add(currentString);
            }

            Console.WriteLine(numbers.Count);
        }
    }
}

IndividualAddingToListBenchmark

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace IndividualAddingToListBenchmark
{
    [Config(typeof(Config))]
    [MemoryDiagnoser]
    [DisassemblyDiagnoser(printAsm: true, printSource: true)]
    public class UnderTest
    {
        private class Config : ManualConfig
        {
            // Force just one iteration, with 1 ops/iteration
            public Config()
            {
                Add(Job.Default.WithInvocationCount(1).WithUnrollFactor(1).WithIterationCount(20));
            }
        }

        const int noNumbers = 10000000; // 10 mil
        private int[] PowersOfTwo = null;
        private int NumberOfArraysRequiredForNoNumbers = 0;
        int[] masterSourceArray = null;
        int[] methodCallBenchmarkNumber = null;
        int indexForMethodCallBenchmark;

        public UnderTest()
        {
            // Build an array storing the powers of 2 that interest us in term of inner
            //  array length, so no time is wasted within the benchmarked methods
            //  themselves for computing this

            // The largest array generated must be able to store our target number of numbers
            int LargestPowerOfTwoNeededToFit = (int)Math.Ceiling(Math.Log(noNumbers, 2));
            int[] powersOfTwo = new int[LargestPowerOfTwoNeededToFit - 1];

            for (int i = 2; i <= LargestPowerOfTwoNeededToFit; i++)
            {
                powersOfTwo[i - 2] = (int)Math.Pow(2, i);
                Console.WriteLine("{0}: {1}", i - 1, powersOfTwo[i - 2]);
            }

            PowersOfTwo = powersOfTwo;
            NumberOfArraysRequiredForNoNumbers = LargestPowerOfTwoNeededToFit - 1;

            Random random = new Random(1);
            // Force the gen buffer for LOH to be big, so we don't have GCs within
            //  the iterations, by allocating a large enough int[] array
            //  at the class level. The GC time will be larger, of course, but we
            //  don't care for just the sample of allocating them, since
            //  the GCs occur in-between iterations. It'll serve a dual purpose as
            //  well: a source for the copying-to-arrays benchmark. Unlike the 
            //  case of the ArrayList, there's no need to go double the no of elements
            masterSourceArray = new int[noNumbers];

            for (int i = 0; i < noNumbers; i++)
            {
                masterSourceArray[i] = random.Next(10);
            }
            
        }
        
        [Benchmark]
        public void OnlyDefineOneListOfInt()
        {
            List<int> numbers = new List<int>(noNumbers);
        }

        
        [Benchmark]
        public void DefineAllIntArraysEnRoute()
        {
            int[] numbers = null;

            for (int i = 0; i < NumberOfArraysRequiredForNoNumbers; i++)
            {
                //Console.WriteLine("Creating an int[] of length: {0}", PowersOfTwo[i]);
                numbers = new int[PowersOfTwo[i]];
            }

            // Keep a reference so that DCE doesn't kick in
            int dummy = numbers.Length;
        }
        
        [Benchmark]
        public void DefineIntArraysEnRouteAndCopyDataToThem()
        {
            // Create an int[] array that is double the previous number of elements
            //  and copy those elements across. Start from the array with 8 elements,
            //  therefore from i=1

            int[] numbers = null;

            for (int i = 1; i < NumberOfArraysRequiredForNoNumbers; i++)
            {
                //Console.WriteLine("Create an int[] array of size: {0}", PowersOfTwo[i]);
                numbers = new int[PowersOfTwo[i]];
                //Console.WriteLine("Copy across: {0} no of elements", PowersOfTwo[i-1]);
                Array.Copy(masterSourceArray, 0, numbers, 0, PowersOfTwo[i - 1]);
            }

            // Keep a reference so that DCE doesn't kick in
            int dummy = numbers.Length;
        }
        
        [Benchmark]
        public void DefineIntArraysEnRouteCopyDataToThemAndFillRestWithSameElement()
        {
            // Create an int[] array that is double the previous number of elements
            //  and copy them across. For the other half of the new array, simply fill
            //  it with the same pre-computed int value. This will simulate the
            //  behavior of the parameterless-constructed List<int> and adding the same
            //  element, allowing us to compare the performance for the same operation. Start
            //  from the array with 8 elements, therefore from i=1

            int[] numbers = null;
            int preCannedIntValue = 1;

            for (int i = 1; i < NumberOfArraysRequiredForNoNumbers; i++)
            {
                //Console.WriteLine("Create an int[] array of size: {0}", PowersOfTwo[i]);
                numbers = new int[PowersOfTwo[i]];
                //Console.WriteLine("Copy across: {0} no of elements", PowersOfTwo[i-1]);
                Array.Copy(masterSourceArray, 0, numbers, 0, PowersOfTwo[i - 1]);
                // Use the array's explicit length in order to help BCE (bound check elimination)
                int maxBracket = numbers.Length < noNumbers ? numbers.Length : noNumbers;
                //Console.WriteLine("Fill the 2nd half of the new object[] array: {0} no of elements", maxBracket-PowersOfTwo[i-1]);
                for (int j = PowersOfTwo[i - 1]; j < maxBracket; j++)
                {
                    numbers[j] = preCannedIntValue;
                }
            }

            // Keep a reference so that DCE doesn't kick in
            int dummy = numbers.Length;
        }

        [Benchmark]
        public void AddSameElementToListOfIntWithParameterlessConstructor()
        {
            List<int> numbers = new List<int>();
            int preCannedIntValue = 1;

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

        [Benchmark]
        public void DefineIntArraysEnRouteCopyDataToThemAndFillRestWithSameElement_byCalling()
        {
            // Create an int[] array that is double the previous number of elements
            //  and copy them across. For the other half of the new array, simply fill
            //  it with the same pre-computed int value. This will simulate the
            //  behavior of the parameterless-constructed List<int> and adding the same
            //  element, allowing us to compare the performance for the same operation. Start
            //  from the array with 8 elements, therefore from i=1
            int preCannedIntValue = 1;

            for (int i = 1; i < NumberOfArraysRequiredForNoNumbers; i++)
            {
                //Console.WriteLine("Create an int[] array of size: {0}", PowersOfTwo[i]);
                methodCallBenchmarkNumber = new int[PowersOfTwo[i]];
                //Console.WriteLine("Copy across: {0} no of elements", PowersOfTwo[i-1]);
                Array.Copy(masterSourceArray, 0, methodCallBenchmarkNumber, 0, PowersOfTwo[i - 1]);
                // Use the array's explicit length in order to help BCE (bound check elimination)
                int maxBracket = methodCallBenchmarkNumber.Length < noNumbers ? methodCallBenchmarkNumber.Length : noNumbers;
                //Console.WriteLine("Fill the 2nd half of the new object[] array: {0} no of elements", maxBracket-PowersOfTwo[i-1]);
                for (indexForMethodCallBenchmark = PowersOfTwo[i - 1]; indexForMethodCallBenchmark < maxBracket; indexForMethodCallBenchmark++)
                {
                    ManuallyInvokedAddMethod(preCannedIntValue);
                }
            }
        }

        // Prevent inlining so we make sure the JIT calls this method
        //  every single time, so that it mimics the real List<int> Add
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void ManuallyInvokedAddMethod(int preCannedIntValue)
        {
            methodCallBenchmarkNumber[indexForMethodCallBenchmark] = preCannedIntValue;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<UnderTest>();
        }
    }
}