Dot Net Perls

Lazy Evaluation in LINQ

by Sam Allen - Updated June 13, 2009

Problem. You want to find out when lazy evaluation is slow, and how you can improve it. You have a LINQ query with an expensive query. This operation may take milliseconds to execute each time. You can improve the performance with caching. Solution. Here we see what problems can be fixed by reducing lazy evaluation in LINQ.

1. Evaluation in LINQ

You can force immediate evaluation to improve performance sometimes. On the other hand, the coolest thing about LINQ queries is that they doesn't do anything until you actually iterate through results. This is called lazy evaluation. Here is an example query that could be cached.

//
// 1.
// Example LINQ query that is lazy.
//
var groupList = from groupItem in _site.Pages
                orderby _site.Categories[groupItem.Category], groupItem.Title
                where groupItem.Visibility == VisibilityType.Regular
                group groupItem by _site.Categories[groupItem.Category];

2. Lazy evaluation in LINQ

The above query doesn't do anything immediately. It is evaluated "lazily." It isn't always important to know how lazy the queries are or their implemention. Next we evaluate the above query.

StringBuilder builder = new StringBuilder();
foreach (var group in groupList)
{
    //
    // Query is evaluated now.
    //
    builder.Append("String");

    foreach (SitePage page in group)
    {
        //
        // Query is evaluated.
        //
        builder.Append("String");
    }
}

3. Forcing immediate evaluation with ToArray

I tried to cache the results in an IEnumerable<IGrouping<string, SitePage>>. That didn't work, because the IEnumerable doesn't force immediate evaluation.

You can use ToArray. To force lazy evaluation, we can use the ToArray extension method. The ToArray method forces the LINQ query to be fully evaluated and stored in an array.

/// <summary>
/// 2.
/// The collection is cached.
/// </summary>
IGrouping<string, SitePage>[] _groupCache;

/// <summary>
/// Generate the HTML (contains the query string).
/// </summary>
public string GetSidebarString()
{
    if (_groupCache == null)
    {
        //
        // Look at how the ToArray() method is called.
        //
        _groupCache = (from groupItem in _site.Pages
                      orderby _site.Categories[groupItem.Category],
                      groupItem.Title
                      where groupItem.Visibility == VisibilityType.Regular
                      group groupItem by _site.Categories[groupItem.Category]
                      ).ToArray();
    }
    StringBuilder builder = new StringBuilder();
    foreach (var group in _groupCache)
    {
        builder.Append("...");
        foreach (SitePage page in group)
        {
            builder.Append("...");
        }
    }
    return builder.ToString();
}

Use an IGrouping collection. We store the collection as a member variable. It is a cache of the LINQ query. Then, we only run the LINQ query when that IGrouping is null. This way, the LINQ is evaluated exactly once.

4. Does this improve performance?

It may, depending on how often the code is run. I cut the time required for the query by a factor of 6 by forcing immediate evaluation and caching the results.

1 - Lazy evaluation with var
Time in ms
2 - Cached array of IGrouping
Time in ms
0.2500.055

5. Summary

By carefully examining the behavior of LINQ, we learn more about ways to enhance its usefulness. By micro-benchmarking, we can become experts on what's really happening in our code. If something is slow, it may be doing something you are not aware of. [Why Benchmark C# - dotnetperls.com]

Dot Net Perls
LINQ | Enumerable.Range | Sum Method | ToDictionary Method | Var Examples | XElement Example
C# | Dictionary StringComparer Tip | DateTime.TryParse Example | Reflection Field Example | Validate Characters in String
© 2009 Sam Allen. All rights reserved.