Published on 09-07-2015

Looking into LINQ Performance

Sometimes performance can be a key factor for the code we write, and that’s when LINQ may not always be the best choice. This post looks at LINQ performance compared to the more traditional ways of writing code.

Code written in LINQ is short, effective and reader friendly, and therefore often the preferred way to query collections of objects in .NET. But if performance is a main concern, it can be necessary to look at just how fast LINQ executes compared to more traditional approaches based on “old fashioned” code loops and if-conditions. Have a look at the below figure, which shows a LINQ approach and a traditional approach to sum up an array of numbers with values less than or equal to 3.

Code examples showing how the same functionality can be achieved with LINQ and traditional loops and if-conditions.
Figure 1: First example uses LINQ and second uses loop and if-condition to achieve the same.

The output in both cases is this: "The sum is: 8"

In the following I will be looking at 3 different scenarios of rising complexity, where I compare performance between code written with LINQ and the equivalent functionality written with tradional code. This way we'll see just how big a difference there are between the two approaches. Code execution speed will of course vary from computer to computer, so the interesting point is to see how the results of each scenario relate to each other.

The hardware used to execute the code in this tutorial is as follows: Lenovo ThinkPad T540p running Windows 7 with 8Gb of RAM.

Scenario 1

Scenario 1 is a code example which counts all numbers with a value of 12 from an array with random numbers.

Code complexity: LINQ with 2 chains (Where and Count).

Code description:

// Create array with 10,000,000 random integer values between 0-20
Random randNum = new Random();
int[] numbers = Enumerable
    .Repeat(0, 10000000)
    .Select(i => randNum.Next(0, 20))
    .ToArray();

// Start stopwatch
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();

// LINQ approach
var cntLinq = numbers
    .Where(n => n == 12)
    .Count();

// Stop stopwatch
stopwatch.Stop();

// Print result
Console.WriteLine("   Time elapsed for LINQ approach: {0} ms", stopwatch.ElapsedMilliseconds);

// Restart stopwatch 
stopwatch.Reset();
stopwatch.Start();

// Traditional approach
int cntTraditional = 0;
foreach (int n in numbers)
    if (n == 12)
        ++cntTraditional;

// Stop stopwatch
stopwatch.Stop();

// Print result
Console.WriteLine("   Time elapsed for traditional approach: {0} ms", stopwatch.ElapsedMilliseconds);

// Make sure both approaches give same result
Debug.Assert(cntLinq == cntTraditional);

Output is:

Output from code in scenario 1.

Thus the traditional approach in this case is approximately 4 times faster than that of LINQ.

Scenario 2

Scenario 2 is a code example which first finds values larger than or equal to 5 from an array with random numbers, multiplies each of the found values by themselves, and then calculates the sum.

Code complexity: LINQ with 3 chains (Where, Select and Sum).

Code description:

// Create array with 10,000,000 random integer values between 0-20
Random randNum = new Random();
int[] numbers = Enumerable
    .Repeat(0, 10000000)
    .Select(i => randNum.Next(0, 20))
    .ToArray();

// Start stopwatch
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();

// LINQ approach
var sumLinq = numbers
    .Where(n => n >= 5)
    .Select(n => n * n)
    .Sum();

// Stop stopwatch
stopwatch.Stop();

// Print result
Console.WriteLine("   Time elapsed for LINQ approach: {0} ms", stopwatch.ElapsedMilliseconds);

// Restart stopwatch
stopwatch.Reset();
stopwatch.Start();

// Traditional approach
int sumTraditional = 0;
foreach (int n in numbers)
    if (n >= 5)
        sumTraditional += n * n;

// Stop stopwatch
stopwatch.Stop();

// Print result
Console.WriteLine("   Time elapsed for traditional approach: {0} ms", stopwatch.ElapsedMilliseconds);

// Make sure both approaches give same result
Debug.Assert(sumLinq == sumTraditional);

Output is:

Output from code in scenario 2.

Thus the traditional approach is this time approximately 4 times faster.

Scenario 3

Scenario 3 is a code example which first finds values between 1-5 from an array with random numbers, joins with array of anonymously typed elements by number, takes spelled out numbers from join result being exactly three letters of length, and finally counts these.

Code complexity: LINQ with 4 chains (Where, Join, Select and Count).

Code description:

// Create array with 10.000.000 random integer values between 0-20
Random randNum = new Random();
int[] numbers = Enumerable
    .Repeat(0, 10000000)
    .Select(i => randNum.Next(0, 20))
    .ToArray();

// Create an array of anonymously typed elements with numbers 1-5, and spelled out values 
var words = new[] {
    new { number = 1, word = "one" },
    new { number = 2, word = "two" },
    new { number = 3, word = "three" },
    new { number = 4, word = "four" },
    new { number = 5, word = "five" },
};

// Start stopwatch
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();

// LINQ approach
var cntLinq = numbers
    .Where(n => n >= 1 && n <= 5)
    .Join(words, n => n, w => w.number, (n, w) => new { w.word })
    .Select(w => w.word.Length == 3)
    .Count();

// Stop stopwatch
stopwatch.Stop();

// Print result
Console.WriteLine("   Time elapsed for LINQ approach: {0} ms", stopwatch.ElapsedMilliseconds);

// Restart stopwatch 
stopwatch.Reset();
stopwatch.Start();

// Traditional approach
int cntTraditional = 0;
List lstWords = new List();
foreach (int n in numbers)
    if (n >= 1 && n <= 5)
    {
        lstWords.Add(words[n - 1].word);
        ++cntTraditional;
    }

// Stop stopwatch
stopwatch.Stop();

// Print result
Console.WriteLine("   Time elapsed for traditional approach: {0} ms", stopwatch.ElapsedMilliseconds);

// Make sure both approaches give same result
Debug.Assert(cntLinq == cntTraditional);

Output is:

Output from code in scenario 3.

Hence the traditional approach is this time approximately 3 times faster.

Summary

For comparison, here are the results from all three scenarios.

Scenario Complexity LINQ approach Traditional approach Difference
Scenario 1 Simple (3 LINQ chains: Where and Count) 121 ms 33 ms 4 : 1
Scenario 2 Medium (4 LINQ chains: Where, Select and Sum) 235 ms 57 ms 4 : 1
Scenario 3 Advanced (5 LINQ chains: Where, Join, Select and Count) 366 ms 119 ms 3 : 1
Average - 240 ms 70 ms 3½ : 1

In all 3 scenarios we found a performance gain of between 3-4 times by the use of the traditional code approach. Even though the results will naturally vary depending on the code style of the programmer and the execution speed of the computer, a clear tendency surdenly does show. This is worth considering, if you are to develop a performance critical piece of code.