Array vs. List Performance
System.Collections.List<T>
is used everywhere in C# code. Except for very special cases, it’s the replacement for arrays, linked lists, queues, and most other one-dimensional data structures. This is because it has all kinds of extra functionality, including the ability to grow in size on-demand. Today’s article wonders about how much performance is lost to gain this convenience and tests the List<T>
class against the lowly C# array: T[]
. How much performance are you giving up with List
and why is that happening? Read on to find out!
Today’s test is actually very simple. We’re just going to test reading from and writing to an array and a List
. Operator overloading makes them even look the same:
// Read val = array[i]; val = list[i]; // Write array[i] = val; list[i] = val;
Here’s the test script:
using System.Collections.Generic; using UnityEngine; public class TestScript : MonoBehaviour { private const int NumIterations = 10000; private const int Length = 10000; private string report; void Start() { var stopwatch = new System.Diagnostics.Stopwatch(); int sum; var array = new int[Length]; var list = new List<int>(Length); for (var i = 0; i < Length; ++i) { array[i] = i; list.Add(i); } stopwatch.Reset(); stopwatch.Start(); for (var it = 0; it < NumIterations; ++it) { sum = 0; for (var i = 0; i < Length; ++i) { sum += array[i]; } } var arrayReadTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var it = 0; it < NumIterations; ++it) { for (var i = 0; i < Length; ++i) { array[i] = i; } } var arrayWriteTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var it = 0; it < NumIterations; ++it) { sum = 0; for (var i = 0; i < Length; ++i) { sum += list[i]; } } var listReadTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (var it = 0; it < NumIterations; ++it) { for (var i = 0; i < Length; ++i) { list[i] = i; } } var listWriteTime = stopwatch.ElapsedMilliseconds; report = "Test,Array Time,List Time\n" + "Read," + arrayReadTime + "," + listReadTime + "\n" + "Write," + arrayWriteTime + "," + listWriteTime; } void OnGUI() { GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report); } }
If you want to try out the test yourself, simply paste the above code into a TestScript.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:
- 2.3 Ghz Intel Core i7-3615QM
- Mac OS X 10.10.3
- Unity 5.0.2f1, Mac OS X Standalone, x86_64, non-development
- 640×480, Fastest, Windowed
And got these results:
Test | Array Time | List Time |
---|---|---|
Read | 230 | 340 |
Write | 65 | 452 |
Reading from a List
takes 47% longer than reading from an array. That’s a really big gap, but not as big as writing where List
takes a whopping 695% longer!
Each of the above tests involved 100 million reads or writes, so the individual cost is extremely low. A few reads and writes won’t matter at all. In fact, the test computer would need to make 258,000 writes in order for switching from List
to array to save one millisecond. A slower machine or one trying to achieve a very high framerate (e.g. for VR) might hit the limit in the tens of thousands of writes rather than the hundreds of thousands. The decision to switch should depend highly on what work your app is doing.
So why is List
so much slower? To find out, let’s decompile the mscorlib.dll
that comes with Unity to look at its implementation:
public T this[int index] { get { if ((uint) index >= (uint) this._size) throw new ArgumentOutOfRangeException("index"); return this._items[index]; } set { this.CheckIndex(index); if (index == this._size) throw new ArgumentOutOfRangeException("index"); this._items[index] = value; } } private void CheckIndex(int index) { if (index < 0 || (uint) index > (uint) this._size) throw new ArgumentOutOfRangeException("index"); }
First, List
implements the IList
interface’s slow virtual function. Second, all index values passed to either read or write are checked to make sure they’re within the bounds of the array with an exception possible in case they’re not. For reading this is just a simple if
, but that branching shows up as quite costly in the test. For writing, there is both an if
and a non-virtual function call to CheckIndex
which has another if
with two comparisons in it and yet-another possible exception.
With this in mind we can make a more informed decision when deciding between List
and arrays. In general, List
is fine to use and certainly much more convenient. However, if you find yourself using them thousands of times per frame you should probably consider switching to an array to skip the function calls and if
statements.
Have you run into trouble with List
or any of the other .NET classes in System.Collections
? How’d you work around it? Let me know in the comments!
#1 by Brad on June 1st, 2015 ·
As a Visual Studio console application (release build):
Array read: 196
List read: 460
Array write: 190
List write: 360
#2 by jackson on June 1st, 2015 ·
Thanks for the data point. The
List
implementation in Microsoft .NET may vary from the one included with Unity’s Mono. Probably more importantly, the Microsoft .NET runtime VM definitely differs from Unity’s Mono runtime. There may also be CPU differences between our test machines and, since you’re running Visual Studio, definitely OS differences. The fact that you got such slower read times highlights just how different the results can be from the same code running in a different environment and how important it is to test on each environment you’re targeting. At the same time, our results aren’t wildly different. That’s good because testing on every target environment is usually quite a slow process if you have many.#3 by Benjamin Guihaire on June 1st, 2015 ·
also, if you want to push the limits of your perfs with read and write in an array.. you might want to use pointers in an unsafe statement.. but here again, unfortunately, its not possible to use pointers with the List class (fixed (int* p = &myList[0] does not compile), but with arrays you can..
time to create an optimized version of List which offer the reallocation convenience and read write speed?
#4 by jackson on June 1st, 2015 ·
Good suggestion, and that is definitely a possibility. The overloaded index operator I decompiled in the article could be easily optimized, so perhaps there are more opportunities throughout the class.
#5 by Benjamin Guihaire on June 7th, 2015 ·
If you really want to, it is possible to get the internal array of a List… (really ugly… but possible)
And if your list is a Vector3, its also possible to iterate directly (also very ugly solution), but when speed matters…
#6 by jackson on June 8th, 2015 ·
These are great ideas! I’ve adopted and improved the internal array getting code into a reusable extension method in today’s article: Optimizing Arrays and Lists.
#7 by David Barlia on September 10th, 2018 ·
For the write comparison, you might consider adding a third test where you specify the capacity of the List. Supposedly the difference is quite significant, presumably because the List wouldn’t have to be repeatedly resized.
#8 by jackson on September 10th, 2018 ·
The capacity of the
List<T>
is passed into the constructor on this line of the test:This will prevent any dynamic resizing during the test.
#9 by David Barlia on September 11th, 2018 ·
–Doh!– I should’ve read more closely.
…Wow! I would’ve thought the primary cause of performance difference between Array and List was that List can dynamically resize. I’m surprised the virtual implementation and a few if statements could have such an impact.
Have you compared the use of an IList referenced to an Array? I suppose that would end up performing like an actual List
#10 by jackson on September 11th, 2018 ·
I haven’t specifically tested
IList
, but it’s performance should be the same when IL2CPP can devirtualize the calls to it and worse when it can’t.#11 by G. on October 2nd, 2018 ·
Well I hope they changed the code to this because it looks childish…
Only one test is required in the write:
“That’s it”… I really don’t understand why this leverage of programming skills “to the bottom” just because we got huge CPUs and we need to hire tons of programmers to piss tons of stupid code.
Sadly, this kind of insight in the source code makes me depressive…
#12 by jackson on October 2nd, 2018 ·
I think the code snippet you posted was partially lost due to HTML formatting and as a result I’m not sure what you’re commenting on. Could you post it again to clarify?
#13 by G. on October 2nd, 2018 ·
Woops I might have done an involuntary code injection with my code snippet. It’s all broken… Maybe the “less than” sign… If you can correct it. Sorry.
#14 by jackson on October 2nd, 2018 ·
No problem. I fixed the formatting. :)
#15 by Johnny Boy on December 18th, 2018 ·
Here’s an article that does performance benchmarks on the various C# collections:
http://cc.davelozinski.com/c-sharp/fastest-collection-for-string-lookups
while doing string look ups. A must read for anyone who needs speed!
#16 by Jirka on June 18th, 2020 ·
I can confirm abysmal big ratio on read. I run few tests on .NET Core console app (release).
See the results.
Test | Array | List
——————————–
Read | 30 | 66
Write | 23 | 164 ≈ 713 %
Read | 37 | 54
Write | 24 | 163 ≈ 679 %
Read | 30 | 60
Write | 23 | 162 ≈ 704 %
#17 by Jirka on June 21st, 2020 ·
… on write, of course.