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>();
}
}
}