As promised earlier in the week, here are the results of benchmarking for
and foreach
.
For each of int
and double
, I created an array and a List<T>
, filled it with random data (the same for the list as the array) and ran each of the following ways of summing the collection:
- A simple for loop, testing the index against the array length / list count each time
- A for loop with an initial variable remembering the length of the array, then comparing the index against the variable each time
- A foreach loop against the collection with the type known at compile and JIT time
- A foreach loop against the collection as
IEnumerable<T>
- Enumerable.Sum
I won't show the complete code in this post, but it's you can download it and then build it against the benchmarking framework. Here's a taste of what it looks like - the code for a list instead of an array, and double
instead of int
is pretty similar:
.Select(x => rng.Next(100))
.ToList();
int[] intArray = intList.ToArray();
var intArraySuite = TestSuite.Create("int[]", intArray, intArray.Sum())
.Add(input => { int sum = 0;
for (int i = 0; i < input.Length; i++) sum += input;
return sum;
}, "For")
.Add(input => { int sum = 0; int length = input.Length;
for (int i = 0; i < length; i++) sum += input;
return sum;
}, "ForHoistLength")
.Add(input => { int sum = 0;
foreach (int d in input) sum += d;
return sum;
}, "ForEach")
.Add(IEnumerableForEach)
.Add(Enumerable.Sum, "Enumerable.Sum")
.RunTests();
...
static int IEnumerableForEach(IEnumerable<int> input)
{
int sum = 0;
foreach (int d in input)
{
sum += d;
}
return sum;
}
(I don't normally format code quite like that, and wouldn't even use a lambda for that sort of code - but it shows everything quite compactly for the sake of blogging.)
Before I present the results, a little explanation:
- I considered
int
anddouble
entirely separately - so I'm not comparing theint[]
results against thedouble[]
results for example. - I considered array and list results together - so I am comparing iterating over an
int[]
with iterating over aList<int>
. - The result for each test is a normalized score, where 1.0 means "the best of the
int
summers" or "the best of thedouble
summers" and other scores for that type of summation show how much slower that test ran (i.e. a score of 2.0 would mean it was twice as slow - it got through half as many iterations). - I'm not currently writing out the number of iterations each one completes - that might be interesting to see how much faster it is to sum ints than doubles.
Happy with that? Here are the results...
============ double[] ============
For 1.00
ForHoistLength 1.00
ForEach 1.00
IEnumerableForEach 11.47
Enumerable.Sum 11.57
============ List<double> ============
For 1.99
ForHoistLength 1.44
ForEach 3.19
IEnumerableForEach 18.78
Enumerable.Sum 18.61
-------------------- Ints --------------------
============ int[] ============
For 1.00
ForHoistLength 2.03
ForEach 1.36
IEnumerableForEach 15.22
Enumerable.Sum 15.73
============ List<int> ============
For 2.82
ForHoistLength 3.49
ForEach 4.78
IEnumerableForEach 25.71
Enumerable.Sum 26.03
I found the results interesting to say the least. Observations:
- When summing a double[] any of the obvious ways are good.
- When summing an int[] there's a slight benefit to using a for loop, but don't try to optimise it yourself - I believe the JIT recognizes the for loop pattern and removes array bounds checking, but not when the length is hoisted. Note the lack of difference when summing doubles - I suspect that this is because the iteration part is more significant when summing ints because integer addition is blindingly fast. This is important - adding integers is about as little work as you're liikely to do in a loop; if you're doing any real work (even as trivial as adding two doubles together) the difference between for and foreach is negligible
- Our IEnumerableForEach method has pretty much the same performance as Enumerable.Sum - which isn't really surprising, as it's basically the same code. (At some point I might include Marc Gravell's generic operators to see how they do.)
- Using a general IEnumerable<T> instead of the specific List<T> makes a pretty huge difference to the performance - I assume this is because the JIT inlines the List<T> code, and it doesn't need to create an object because List<T>.Enumerator is a value type. (The enumerator will get boxed in the general version, I believe.)
- When using a for loop over a list, hosting the length in the for loop helped in the double version, but hindered in the int version. I've no idea why this happens.
If anyone fancies running this on their own box and letting me know if they get very different results, that would be really interesting. Likewise let me know if you want me to add any more tests into the mix.
...Full Article.
No comments:
Post a Comment
Post your comments here: