A thread came up yesterday on the C# newsgroup about when to use the “normal”
and when to use
List<T>.ForEach (assuming, of course, that one is dealing with a
List<T> in the first place). Obviously there are readability issues, but we ended up focusing
on performance. (Isn’t that telling in its own right? How often is the iteration part rather than
the body going to dominate and be a significant bottleneck? Anyway, I digress.)
So, I wrote a small benchmark, and Patrick asked me to blog about it. I’ve refactored the test I posted on the newsgroup and added a couple more tests as suggested by Willy Denoyette. The source code is a little bit unwieldy (and frankly tedious) to include in this blog post – download it if you’re interested.
The test basically creates a list of strings, each being “x”. Each test case iterates through the
list a fixed number of times, keeping a running total of the lengths of strings it sees. The result
is checked and the time taken is reported. This is what the individual tests do:
foreach (string x in list)in the obvious way.
NewDelegateEachTimeuses an anonymous method as the parameter to
List.ForEach<T>, where that method captures a different variable each “outer” iteration. That means a new delegate has to be created each time.
CachedDelegatecreates a single delegate and uses that for all calls to
LanguageForEachWithCopy1copies the list to an array each “outer” iteration, and then uses
foreachover that array.
LanguageForEachWithCopy2copies the list to an array once at the start of the test, and then uses
foreachover that array.
Here are the results, with a few different test cases (all doing the same amount of work overall). I shall attempt to tabulate them a bit better when I get some time :)
Test parameters: Size=10000000; Iterations=100 Test 00:00:11.8251914: LanguageForEach Test 00:00:05.3463387: NewDelegateEachTime Test 00:00:05.3238162: CachedDelegate Test 00:00:22.1342570: LanguageForEachWithCopy1 Test 00:00:03.7493164: LanguageForEachWithCopy2
Test parameters: Size=1000000; Iterations=1000 Test 00:00:11.8163135: LanguageForEach Test 00:00:05.3392333: NewDelegateEachTime Test 00:00:05.3334596: CachedDelegate Test 00:00:26.9471681: LanguageForEachWithCopy1 Test 00:00:03.5251209: LanguageForEachWithCopy2
Test parameters: Size=100000; Iterations=10000 Test 00:00:11.6576344: LanguageForEach Test 00:00:05.2225531: NewDelegateEachTime Test 00:00:05.2066938: CachedDelegate Test 00:00:16.2563401: LanguageForEachWithCopy1 Test 00:00:03.0949064: LanguageForEachWithCopy2
Test parameters: Size=100; Iterations=10000000 Test 00:00:12.2547105: LanguageForEach Test 00:00:04.9791093: NewDelegateEachTime Test 00:00:04.6191521: CachedDelegate Test 00:00:06.0731525: LanguageForEachWithCopy1 Test 00:00:02.8182444: LanguageForEachWithCopy2
LanguageForEachWithCopy1 results surprised me, as I’d really expected the
performance to go up as the number of iterations went up. It seems it’s cheaper to copy
a short list many times than a long list a few times…