【问题标题】:Why is it faster if I put an extra ToArray before ToLookup?为什么我在 ToLookup 之前放一个额外的 ToArray 会更快?
【发布时间】:2019-11-21 18:46:03
【问题描述】:

我们有一个将 .csv 文件解析为查找的简短方法:

ILookup<string, DgvItems> ParseCsv( string fileName )
{
    var file = File.ReadAllLines( fileName );
    return file.Skip( 1 ).Select( line => new DgvItems( line ) ).ToLookup( item => item.StocksID );
}

以及DgvItems的定义:

public class DgvItems
{
    public string DealDate { get; }

    public string StocksID { get; }

    public string StockName { get; }

    public string SecBrokerID { get; }

    public string SecBrokerName { get; }

    public double Price { get; }

    public int BuyQty { get; }

    public int CellQty { get; }

    public DgvItems( string line )
    {
        var split = line.Split( ',' );
        DealDate = split[0];
        StocksID = split[1];
        StockName = split[2];
        SecBrokerID = split[3];
        SecBrokerName = split[4];
        Price = double.Parse( split[5] );
        BuyQty = int.Parse( split[6] );
        CellQty = int.Parse( split[7] );
    }
}

我们发现如果我们在ToLookup() 之前添加一个额外的ToArray(),如下所示:

static ILookup<string, DgvItems> ParseCsv( string fileName )
{
    var file = File.ReadAllLines( fileName  );
    return file.Skip( 1 ).Select( line => new DgvItems( line ) ).ToArray().ToLookup( item => item.StocksID );
}

后者明显更快。更具体地说,当使用140万行的测试文件时,前者大约需要4.3秒,后者大约需要3秒。

我预计ToArray() 应该需要额外的时间,所以后者应该会稍微慢一些。为什么它实际上更快?


额外信息:

  1. 我们发现了这个问题,因为还有另一种方法可以将相同的 .csv 文件解析为不同的格式,并且需要大约 3 秒,因此我们认为这个方法应该能够在 3 秒内完成相同的操作。

  2. 原始数据类型为Dictionary&lt;string, List&lt;DgvItems&gt;&gt;,原始代码没有使用linq,结果类似。


BenchmarkDotNet 测试类:

public class TestClass
{
    private readonly string[] Lines;

    public TestClass()
    {
        Lines = File.ReadAllLines( @"D:\20110315_Random.csv" );
    }

    [Benchmark]
    public ILookup<string, DgvItems> First()
    {
        return Lines.Skip( 1 ).Select( line => new DgvItems( line ) ).ToArray().ToLookup( item => item.StocksID );
    }

    [Benchmark]
    public ILookup<string, DgvItems> Second()
    {
        return Lines.Skip( 1 ).Select( line => new DgvItems( line ) ).ToLookup( item => item.StocksID );
    }
}

结果:

| Method |    Mean |    Error |   StdDev |
|------- |--------:|---------:|---------:|
|  First | 2.530 s | 0.0190 s | 0.0178 s |
| Second | 3.620 s | 0.0217 s | 0.0203 s |

我在原始代码的基础上做了另一个测试。看来问题不在 Linq 上。

public class TestClass
{
    private readonly string[] Lines;

    public TestClass()
    {
        Lines = File.ReadAllLines( @"D:\20110315_Random.csv" );
    }

    [Benchmark]
    public Dictionary<string, List<DgvItems>> First()
    {
        List<DgvItems> itemList = new List<DgvItems>();
        for ( int i = 1; i < Lines.Length; i++ )
        {
            itemList.Add( new DgvItems( Lines[i] ) );
        }

        Dictionary<string, List<DgvItems>> dictionary = new Dictionary<string, List<DgvItems>>();

        foreach( var item in itemList )
        {
            if( dictionary.TryGetValue( item.StocksID, out var list ) )
            {
                list.Add( item );
            }
            else
            {
                dictionary.Add( item.StocksID, new List<DgvItems>() { item } );
            }
        }

        return dictionary;
    }

    [Benchmark]
    public Dictionary<string, List<DgvItems>> Second()
    {
        Dictionary<string, List<DgvItems>> dictionary = new Dictionary<string, List<DgvItems>>();
        for ( int i = 1; i < Lines.Length; i++ )
        {
            var item = new DgvItems( Lines[i] );

            if ( dictionary.TryGetValue( item.StocksID, out var list ) )
            {
                list.Add( item );
            }
            else
            {
                dictionary.Add( item.StocksID, new List<DgvItems>() { item } );
            }
        }

        return dictionary;
    }
}

结果:

| Method |    Mean |    Error |   StdDev |
|------- |--------:|---------:|---------:|
|  First | 2.470 s | 0.0218 s | 0.0182 s |
| Second | 3.481 s | 0.0260 s | 0.0231 s |

【问题讨论】:

  • 我高度怀疑测试代码/测量。请贴出计算时间的代码
  • 我的猜测是,如果没有.ToArray(),对.Select( line =&gt; new DgvItems( line ) ) 的调用会在调用ToLookup( item =&gt; item.StocksID ) 之前返回一个IEnumerable。使用 IEnumerable 查找特定元素比使用 Array 更糟糕。转换为数组并执行查找可能比使用 ienumerable 更快。
  • 旁注:使用var file = File.ReadLines( fileName ); - ReadLines 而不是ReadAllLines,你的代码可能会更快
  • 您应该使用BenchmarkDotnet 进行实际性能测量。此外,请尝试隔离您要测量的实际代码,并且不要在测试中包含 IO。
  • 我不知道为什么这会遭到反对 - 我认为这是个好问题。

标签: c# linq


【解决方案1】:

我设法用下面的简化代码复制了这个问题:

var lookup = Enumerable.Range(0, 2_000_000)
    .Select(i => ( (i % 1000).ToString(), i.ToString() ))
    .ToArray() // +20% speed boost
    .ToLookup(x => x.Item1);

创建的元组的成员必须是字符串,这一点很重要。从上面的代码中去掉两个.ToString()就消除了ToArray的优势。 .NET Framework 的行为与 .NET Core 略有不同,因为只需删除第一个 .ToString() 即可消除观察到的差异。

我不知道为什么会这样。

【讨论】:

  • 您用哪个框架确认了这一点?我看不出使用 .net framework 4.7.2 有什么不同
  • @Magnus .NET Framework 4.8(VS 2019,发布版本)
  • 最初我夸大了观察到的差异。它在 .NET Core 中约为 20%,在 .NET Framework 中约为 10%。
  • 不错的复制品。我不知道为什么会发生这种情况,也没有时间弄清楚,但我的 猜测ToArrayToList 强制数据位于连续内存中;在流水线的特定阶段执行此操作,即使它增加了成本,也可能导致稍后的操作有更少的处理器缓存未命中;处理器缓存未命中非常昂贵。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-30
  • 2015-06-20
  • 1970-01-01
相关资源
最近更新 更多