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

LINQ Performance Update Chart

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.