使用Linq注意事项

前言

前一段时间在.Net(Gtihub)社区中看有人说Linq的Count比Where后Count慢两倍,最近这两年在项目中使用Linq的次数越来越多了,毕竟可以减少代码的行数,说Linq是生产力不为过,原先也看到Linq方法的源码(忘记是那个源码了,内部是foreach迭代器),但没有对Linq下的方法进行性能测试.如果是需要性能的地方,还是尽量用for/foreach自己去处理,也可以使用循环展开的方式去优化.对性能没什么要求,那肯定还是使用Linq.

测试代码

namespace CSharpBenchmarks.LinqTest
{
    [MemoryDiagnoser]
    [Orderer(SummaryOrderPolicy.FastestToSlowest)]
    public class CountTest
    {
        private static List<int> Numbers = new(1000);

        static CountTest()
        {
            Random random = new Random(10);
            for (int i = 0; i < 1000; i++)
            {
                Numbers.Add(random.Next(int.MinValue, int.MaxValue));
            }
        }

        [Benchmark]
        public int LinqCount()
        {
            return Numbers.Count(num => num > int.MaxValue / 2);
        }

        [Benchmark]
        public int LinqWhereCount()
        {
            return Numbers.Where(num => num > int.MaxValue / 2).Count();
        }
    }
}

使用BenchmarkDotNet测试结果:

使用BenchmarkDotNet测试Linq下Count和Where后Count

发现Count比Where后Count性能对比,有差不多3倍的差距,Count比Where后Count的优点,就是没有在堆上分配内存.

Count/Where源码阅读

先看Count的源码:
public static int Count<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (source is ICollection<TSource> collectionoft)
    {
        return collectionoft.Count;
    }

    if (source is IIListProvider<TSource> listProv)
    {
        return listProv.GetCount(onlyIfCheap: false);
    }

    if (source is ICollection collection)
    {
        return collection.Count;
    }

    int count = 0;
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
        checked
        {
            while (e.MoveNext())
            {
                count++;
            }
        }
    }

    return count;
}

public static int Count<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (predicate == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
    }

    int count = 0;
    foreach (TSource element in source)
    {
        checked
        {
            if (predicate(element))
            {
                count++;
            }
        }
    }

    return count;
}

在看Where源码:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (predicate == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
    }

    if (source is Iterator<TSource> iterator)
    {
        return iterator.Where(predicate);
    }

    if (source is TSource[] array)
    {
        return array.Length == 0 ?
            Empty<TSource>() :
            new WhereArrayIterator<TSource>(array, predicate);
    }

    if (source is List<TSource> list)
    {
        return new WhereListIterator<TSource>(list, predicate);
    }

    return new WhereEnumerableIterator<TSource>(source, predicate);
}

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (predicate == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.predicate);
    }

    return WhereIterator(source, predicate);
}

private static IEnumerable<TSource> WhereIterator<TSource>(IEnumerable<TSource> source, Func<TSource, int, bool> predicate)
{
    int index = -1;
    foreach (TSource element in source)
    {
        checked
        {
            index++;
        }

        if (predicate(element, index))
        {
            yield return element;
        }
    }
}

Count和Where后Count性能差距是在哪里呢?在Where源码中有判断是否为数组/List/Enumerable,分别交给相应的类型去处理,在这三个类型WhereEnumerableIterator/WhereArrayIterator/WhereListIterator都有一个GetCount实现,GetCount是根据索引下标实现的.

public int GetCount(bool onlyIfCheap)
{
    if (onlyIfCheap)
    {
        return -1;
    }

    int count = 0;

    foreach (TSource item in _source)
    {
        if (_predicate(item))
        {
            checked
            {
                count++;
            }
        }
    }

    return count;
}

在需要性能,自己循环实现

[Benchmark]
public int ForeachCount()
{
    int count = 0;
    int val = int.MaxValue / 2;  //计算放到循环外
    foreach (var num in Numbers)
    {
        if (num > val)
        {
            count += 1;
        }
    }
    return count;
}

在进行一次性能测试:

使用BenchmarkDotNet测试优化代码

秋风 2022-05-29