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.
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];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");
}
}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.
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.250 | 0.055 |
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]