LINQ Performance Update
It’s been over three years since the last article on LINQ performance. That was all the way back in the Unity 5.0 days using Mono as a scripting backend. Today we’ll update that article’s test with Unity 2018.1 and IL2CPP to see how LINQ fares these days. Is it any better? Read on to find out!
The previous article had an example of LINQ usage that included the two most commonly-used functions: Where
and Select
. There are many more, but these are pretty representative of LINQ. To compare to these functions, the test includes a completely “manual” version and a “normal” version that uses the same delegate in a more functional style. The test is now updated to be more straightforward and rely less on delegates to avoid confusing the test framework’s performance with the code being tested. Here’s what the new version of the test looks like:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using UnityEngine; public class TestScript : MonoBehaviour { private string report; void Start() { int[] array = new int[1024]; const int numIterations = 10000; Stopwatch stopwatch = new Stopwatch(); Func<int,bool> checker = val => val == 1; List<int> found = null; stopwatch.Reset(); stopwatch.Start(); for (int it = 0; it < numIterations; ++it) { int len = array.Length; found = new List<int>(len); for (int i = 0; i < len; ++i) { int elem = array[i]; if (elem == 1) { found.Add(elem); } } } long whereManualTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (int it = 0; it < numIterations; ++it) { int len = array.Length; found = new List<int>(len); for (int i = 0; i < len; ++i) { int elem = array[i]; if (checker(elem)) { found.Add(elem); } } } long whereNormalTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (int it = 0; it < numIterations; ++it) { array.Where(checker).ToList(); } long whereLINQTime = stopwatch.ElapsedMilliseconds; Func<int,int> transformer = val => val * 2; List<int> transformed = null; stopwatch.Reset(); stopwatch.Start(); for (int it = 0; it < numIterations; ++it) { int len = array.Length; transformed = new List<int>(len); for (int i = 0; i < len; ++i) { transformed.Add(array[i] * 2); } } long selectManualTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (int it = 0; it < numIterations; ++it) { int len = array.Length; transformed = new List<int>(len); for (int i = 0; i < len; ++i) { transformed.Add(transformer(array[i])); } } long selectNormalTime = stopwatch.ElapsedMilliseconds; stopwatch.Reset(); stopwatch.Start(); for (int it = 0; it < numIterations; ++it) { array.Select(transformer).ToList(); } long selectLINQTime = stopwatch.ElapsedMilliseconds; report = "Test,Manual Time,Normal Time,LINQ Timen" + $"Where,{whereManualTime},{whereNormalTime},{whereLINQTime}n" + $"Select,{selectManualTime},{selectNormalTime},{selectLINQTime}"; // Don't allow these to be optimized away print(transformed + " " + found); } 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 with this test environment:
- 2.7 Ghz Intel Core i7-6820HQ
- Mac OS X 10.13.5
- Unity 2018.1.0f2
- Mac OS X Standalone
- .NET 4.x
- IL2CPP
- .NET Standard 2.0
- Non-development
- 640×480, Fastest, Windowed
And got these results:
Test | Manual Time | Normal Time | LINQ Time |
---|---|---|---|
Where | 30 | 118 | 120 |
Select | 58 | 135 | 159 |
The “normal” versions that implement LINQ but still use delegates continue to be slightly cheaper than the LINQ versions. The main savings here is just the removal of function calls. The “manual” versions, however, are a ton faster than either other version. Where
times are reduced by about 75% and Select
times drop about 60%. Those are huge savings, but much smaller than before with Unity 5.0 and Mono where the drops were 96% and 90% respectively.
But the tests only show a direct performance comparison. Usability is another matter entirely. LINQ is more usable in some respects because there is language syntax support and promotes what is arguably a more readable, functional style. The “manual” versions are more usable from another perspective. When the user wants to optimize for performance, LINQ provides no options but the “manual” version can be tweaked to break out of loops early, avoid allocations, and so forth.
The takeaway here is that LINQ performance is still quite poor. It’s slightly better these days, but still dramatically worse than simply writing the code yourself.
#1 by VVEthan on July 3rd, 2018 ·
Love it, as always with your performance-check articles!
Also, THANK YOU for the notes about usability at the end — Helping people draw useful conclusions from the data is fantastically valuable.
In this particular case, I have a personal understanding of the usability gains / costs, but your notes make sharing articles like this with others really easy because I don’t have to worry about strange conclusions being drawn like “ban all LINQ statements!”
#2 by some guy on August 20th, 2019 ·
you preallocate the list with the size of your array in manual method. How is that a fair comparison?
#3 by jackson on August 20th, 2019 ·
It’s definitely not an apples-to-apples comparison for that reason. However, it does show the performance that’s attainable when not using LINQ.