C#-聽說有人不推薦使用Linq!?

自笑非發表於2024-07-04

先說結論

linq和直接遍歷的效能差異點主要還是迭代方式陣列字串這類的foreach都是專門有最佳化的,而linq都是通用的泛型呼叫迭代器,如果剛好遇到這類資料又需要高效能就儘量不用linq,其他型別或效能要求不高的還是linq香。(ps:linq寫的不好不能怪linq)

背景

起初是看到有人寫的部落格說***不推薦使用Linq!( https://mp.weixin.qq.com/s/FuF1Wp2eMxbamY5-uMErVQ ),還被多個wx訂閱號轉載,本能地覺得離譜呀!
what?!
unbelievable!

自測

仔細檢視,發現這程式碼寫的有問題呀,然後自己重寫了一版

部分程式碼

    [Benchmark]
    public void SumWithLinq()
    {
        int sum = _row.Sum(c => c == ',' ? 0 : c-'0');
    }

    [Benchmark(Baseline = true)]
    public void SumWithLoop()
    {
        int sum = 0;
        for (int i = 0; i < _row.Length; i++)
        {
            var c= _row[i];
            if (c!= ',')
                sum += c-'0';

        }
    }

測試結果

分析

雖然比那個博主的資料好了很多,但是linq確實比直接使用迴圈慢得多。檢視Sum的內部實現,意料之中是用的迭代器,於是繼續新增測試

再測

新增foreach測試程式碼

    [Benchmark]
    public void SumWithForeach()
    {
        int sum = 0;
        foreach (var c in _row)
        {
            if (c != ',')
                checked
                {
                    sum += int.CreateChecked(c-'0');
                }
        }
    }

測試結果

分析

foreach和for居然沒有什麼區別,那差距是在哪裡呢?突然意識到sum的呼叫方是IEnumerable,於是又新增了如下測試

再測

新增測試程式碼

    [Benchmark]
    public void SumWithEnumerable()
    {
        int sum = 0;
        foreach (var c in _row as IEnumerable<char>)
        {
            if (c != ',')
                checked
                {
                    sum += int.CreateChecked(c-'0');
                }
        }
    }

測試結果

分析

直接對string做foreach和將string轉為可迭代物件再foreach居然差這麼多,string的foreach底層實現可能有最佳化

檢視ILSpy

直接foreach:

呼叫迭代器:

深入瞭解foreach

參閱文件:.NET 本質論 - 瞭解 C# foreach 的內部工作原理和使用 yield 的自定義迭代器
https://learn.microsoft.com/zh-cn/archive/msdn-magazine/2017/april/essential-net-understanding-csharp-foreach-internals-and-custom-iterators-with-yield

附完整程式碼

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

internal class Program
{
    static void Main(string[] args)
    {
        BenchmarkRunner.Run<LinqTest>();
    }
}

[MemoryDiagnoser, MemoryRandomization]
public class LinqTest
{
    private static readonly string _row = "1,2,3,4,5,6,7,8,9,10";

    [Benchmark]
    public void SumWithLinq()
    {
        int sum = _row.Sum(c => c == ',' ? 0 : c-'0');
    }


    [Benchmark]
    public void SumWithEnumerable()
    {
        int sum = 0;
        foreach (var c in _row as IEnumerable<char>)
        {
            if (c != ',')
                checked
                {
                    sum += int.CreateChecked(c-'0');
                }
        }
    }

    [Benchmark]
    public void SumWithForeach()
    {
        int sum = 0;
        foreach (var c in _row)
        {
            if (c != ',')
                checked
                {
                    sum += int.CreateChecked(c-'0');
                }
        }
    }

    [Benchmark(Baseline = true)]
    public void SumWithLoop()
    {
        int sum = 0;
        for (int i = 0; i < _row.Length; i++)
        {
            var c= _row[i];
            if (c!= ',')
                sum += c-'0';

        }
    }
}

相關文章