Basic LINQ Performance
SQL-style LINQ queries are a concise, readable way of performing various tasks dealing with all kinds of collections. Surely all that convenience comes with a performance cost to it. How bad do you think it is? Today we’ll look at the cost of some basic LINQ queries (Where
, Select
) versus the equivalent non-LINQ code. We’ll also see how much slower both of them are compared to manually-written, traditional code that does away with all the flexibility. Read on to see the results!
The following test uses the extension function versions of LINQ found in the System.Linq
namespace. They look very similar to the C# keywords but provide a little more flexibility that allows us to isolate just the parts we want. Here’s a quick example:
someCollection.Where(element => element == 6); // all the elements that are 6 someCollection.Select(element => element * 2); // double each of the elements
We’ll stack those up against hand-written functions that are just as generic:
// Equivalent to Where() private List<TInput> WhereNormal<TInput>( IEnumerable<TInput> enumeration, Func<TInput, bool> checker ) { var found = new List<TInput>(); foreach (var elem in enumeration) { if (checker(elem)) { found.Add(elem); } } return found; } // Equivalent to Select() private List<TResult> SelectNormal<TInput,TResult>( IEnumerable<TInput> enumeration, Func<TInput,TResult> transformer ) { var transformed = new List<TResult>(); foreach (var elem in enumeration) { transformed.Add(transformer(elem)); } return transformed; }
And for the baseline, we’ll use some “manual” functions that are much less generic. They know the enumeration is an array and the type of the elements in the array. They include the selector
or transformer
function directly. Basically, it’s the old-school way of writing specific code for a specific problem. It’s also not a fair fight.
// Manual version of Where() private List<int> WhereManual(int[] array) { var found = new List<int>(); for (int i = 0, len = array.Length; i < len; ++i) { var elem = array[i]; if (elem == 1) { found.Add(elem); } } return found; } // Manual version of Select() private int[] SelectManual(int[] array) { var len = array.Length; var transformed = new int[len]; for (var i = 0; i < len; ++i) { transformed[i] = array[i] * 2; } return transformed; }
Now let’s pit them against each other to see which is fastest. We’ll use an array of 1024 elements—all zero—and look for the value 1
in Where()
and double each element in Select
. Here’s the test program:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using UnityEngine; public static class StopwatchExtensions { public static long RunTests( this Stopwatch stopwatch, int numIterations, Action testFunction ) { stopwatch.Reset(); stopwatch.Start(); for (var i = 0; i < numIterations; ++i) { testFunction(); } return stopwatch.ElapsedMilliseconds; } } public class TestScript : MonoBehaviour { private string report; void Start() { var array = new int[1024]; var numIterations = 10000; var stopwatch = new Stopwatch(); Func<int,bool> checker = val => val == 1; var whereManualTime = stopwatch.RunTests( numIterations, () => WhereManual(array) ); var whereNormalTime = stopwatch.RunTests( numIterations, () => WhereNormal<int>(array, checker) ); var whereLINQTime = stopwatch.RunTests( numIterations, () => WhereLINQ<int>(array, checker) ); Func<int,int> transformer = val => val * 2; var selectManualTime = stopwatch.RunTests( numIterations, () => SelectManual(array) ); var selectNormalTime = stopwatch.RunTests( numIterations, () => SelectNormal<int,int>(array, transformer) ); var selectLINQTime = stopwatch.RunTests( numIterations, () => SelectLINQ<int,int>(array, transformer) ); report = "Test,Manual Time,Normal Time,LINQ Time\n" + "Where," + whereManualTime + "," + whereNormalTime + "," + whereLINQTime + "\n" + "Select," + selectManualTime + "," + selectNormalTime + "," + selectLINQTime + "\n"; } void OnGUI() { GUI.TextArea(new Rect(0, 0, Screen.width, Screen.height), report); } private List<int> WhereManual(int[] array) { var found = new List<int>(); for (int i = 0, len = array.Length; i < len; ++i) { var elem = array[i]; if (elem == 1) { found.Add(elem); } } return found; } private List<TInput> WhereNormal<TInput>( IEnumerable<TInput> enumeration, Func<TInput, bool> checker ) { var found = new List<TInput>(); foreach (var elem in enumeration) { if (checker(elem)) { found.Add(elem); } } return found; } private List<TInput> WhereLINQ<TInput>( IEnumerable<TInput> enumeration, Func<TInput, bool> checker ) { return enumeration.Where(checker).ToList(); } private int[] SelectManual(int[] array) { var len = array.Length; var transformed = new int[len]; for (var i = 0; i < len; ++i) { transformed[i] = array[i] * 2; } return transformed; } private List<TResult> SelectNormal<TInput,TResult>( IEnumerable<TInput> enumeration, Func<TInput,TResult> transformer ) { var transformed = new List<TResult>(); foreach (var elem in enumeration) { transformed.Add(transformer(elem)); } return transformed; } private List<TResult> SelectLINQ<TInput,TResult>( IEnumerable<TInput> enumeration, Func<TInput,TResult> transformer ) { return enumeration.Select(transformer).ToList(); } }
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.2
- Unity 5.0.0f4, Mac OS X Standalone, x86_64, non-development
- 640×480, Fastest, Windowed
And got these results:
Test | Manual Time | Normal Time | LINQ Time |
---|---|---|---|
Where | 13 | 337 | 355 |
Select | 49 | 486 | 584 |
LINQ, to me at least, was surprisingly not much slower than the “normal” version that used a foreach
loop. Considering that much more code needs to be written, maintained, and understood in order to achieve very little performance, the LINQ queries seem like a bargain.
The “manual” tests, however, are a lot faster. That was to be expected since they “cheat” by knowing all of the specifics of the problem: element type, enumeration type, and function to perform at each element. They’re about as much code to write as the “normal” versions, just with less flexibility and way more speed. A 10x performance gain is to be had by dropping the generic code and solving the problem directly and manually.
So when it comes to LINQ queries, know that they’re about an order of magnitude slower than if you were to manually write the code. If you find that a function with a LINQ query is slowing down your app, consider re-writing the LINQ query with the “manual” approach. You may get a huge performance boost!
Have you ever run into a LINQ-specific performance issue? Know of any uses of LINQ that are especially slow? Share your experience in the comments!
#1 by aoakenfo on March 16th, 2015 ·
There seem to be a lot of problems with LINQ on iOS in the community forums. No personal experience with it and not sure if it still applies to Unity 5.
#2 by jackson on March 16th, 2015 ·
The slowest code is the code that doesn’t work. :)
Yes, there seem to be some ways for LINQ to try to use JIT which doesn’t work on iOS. I’ve run into it a couple of times with 4.6, but haven’t tried with 5.0 to see if anything is better with either Mono or IL2CPP.
In any case, the basic LINQ operations like above should be safe. I haven’t found the rhyme or reason as to why some LINQ operations fail, but simple stuff like in this article should be fine to use.