
I’m writing this piece in a response to Jackson Dunstan’s piece about the speed of For vs. Foreach. He got some really peculiar results for using foreach with arrays and I’ve decided that I need to go deeper. Look what I’ve found out.
Before I proceed let me do a small disclaimer:
As part of my work at Caped Koala Studios I’m doing a small marketing push just to reach anyone who might be interested in playing educational games for children. See poraora.com if you are looking for games for your kids or are just interested in what happens in the edutainment space.
Following the discussion in the comments of Jackson’s article about the test’s fairness I’ve decided to tweak it a little bit to make sure we do some work in the loops and the the compiler is not skipping anything.
In my test I’ve changed the following things:
- Changed the object to a simple Counter class with a single int field for both array and list
- Added a single int incrementation to every loop in the test to make sure we do something “nontrivial” there
Here is the code I’ve used:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
using System; using System.Collections.Generic; using UnityEngine; public class ForVsForeach : MonoBehaviour { private const int NumIterations = 100000; private string report; void Start() { report = "Size,Array For Time,Array Foreach Time,List For Time,List Foreach Time\n"; var sizes = new []{10,100,1000}; foreach (var size in sizes) { report += Test(size); } } private class Counter { public int Value; } private string Test(int size) { var array = new Counter[size]; for (int i = 0; i < size; i++) { array[i] = new Counter(); } var list = new List<Counter>(size); list.AddRange(array); var stopwatch = new System.Diagnostics.Stopwatch(); Counter obj = null; stopwatch.Reset(); stopwatch.Start(); for (var iteration = 0; iteration < NumIterations; ++iteration) { for (int i = 0, len = array.Length; i < len; ++i) { obj = array[i]; obj.Value++; } } var arrayForTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var iteration = 0; iteration < NumIterations; ++iteration) { foreach (var cur in array) { cur.Value++; } } var arrayForeachTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var iteration = 0; iteration < NumIterations; ++iteration) { for (int i = 0, len = list.Count; i < len; ++i) { obj = list[i]; obj.Value++; } } var listForTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var iteration = 0; iteration < NumIterations; ++iteration) { foreach (var cur in list) { cur.Value++; } } var listForeachTime = stopwatch.ElapsedMilliseconds; return size + "," + arrayForTime + "," + arrayForeachTime + "," + listForTime + "," + listForeachTime + "\n"; } void OnGUI() { GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report); } private void MethodThatDoesSomethingComplicated(Counter counter) { counter.Value++; counter.Value = (int)(Mathf.Sqrt(100 + counter.Value) + Mathf.Cos(counter.Value)); counter.Value = (int)(Mathf.Sqrt(100 + counter.Value) + Mathf.Cos(counter.Value)); counter.Value = (int)(Mathf.Sqrt(100 + counter.Value) + Mathf.Cos(counter.Value)); } } |
Similarly to how Jackson’s tests are structured – If you want to try out the test yourself, simply paste the above code into a ForVsForeach.cs file in your Unity project’s Assets directory and attach it to the main camera game object in a new, empty project. Then build in non-development mode for 64-bit processors and run it windowed at 640×480 with fastest graphics. I ran it that way on this machine:
- 3.5 GHz Intel Core i7-4770,
- Windows 8.1,
- Unity 5.1.2f1, Windows Standalone, x86_64, non-development,
- 640×480, Fastest, Windowed
And here are the results I got are very different. In my case foreach instead of being twice as fast for arrays turned out to be at best half as fast for both arrays and generic lists (time in ms):
Size | Array For | Array Foreach | List For | List Foreach |
---|---|---|---|---|
10 | 1 | 2 | 3 | 15 |
100 | 10 | 22 | 36 | 85 |
1000 | 96 | 221 | 358 | 801 |
Interestingly the results I’ve got for the x86 (32b) build are very different. Here foreach and for give the same results for arrays but not for lists (time in ms):
Size | Array For | Array Foreach | List For | List Foreach |
---|---|---|---|---|
10 | 1 | 1 | 5 | 16 |
100 | 11 | 12 | 40 | 94 |
1000 | 112 | 115 | 389 | 877 |
After a very brief spark of enthusiasm for using foreach with arrays I’m going back to defaulting to for as the loop of choice. The extra benefit being not having to worry about allocations when using it with generic lists.
I’ve also put a method into the code above for you to test out how complicated the code inside the loop needs to be for this optimization to become irrelevant. This is a reminder of premature optimization concept.
You should be looking for iteration optimizations like using unsafe code loops only in case when your doing really simple things inside the loops. Otherwise you’ll be better off by (preferably) changing the algorithm to a better option or by fiddling with whatever you are doing inside the loop.
Thanks for reading. (pun intended)
P.S. If you are curios what I’m working on currently check out the poraora.com :)
P.S. This turned out to be a two-part series and you can read the second part here :).
Pingback: Loop Performance: For vs. Foreach vs. While « JacksonDunstan.com