Temos um método curto que analisa o arquivo .csv em uma pesquisa:
ILookup<string, DgvItems> ParseCsv( string fileName )
{
var file = File.ReadAllLines( fileName );
return file.Skip( 1 ).Select( line => new DgvItems( line ) ).ToLookup( item => item.StocksID );
}
E a definição de 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] );
}
}
E descobrimos que, se adicionarmos um extra ToArray()
antes ToLookup()
desta forma:
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 );
}
O último é significativamente mais rápido. Mais especificamente, quando o arquivo de teste é usado com 1,4 milhão de linhas, o primeiro leva cerca de 4,3 segundos e o segundo leva cerca de 3 segundos.
Espero ToArray()
levar um tempo extra para que este último seja um pouco mais lento. Por que é realmente mais rápido?
Informação extra:
Encontramos esse problema porque existe outro método que analisa o mesmo arquivo .csv em um formato diferente e leva cerca de 3 segundos. Portanto, achamos que este deve ser capaz de fazer a mesma coisa em 3 segundos.
O tipo de dados original é
Dictionary<string, List<DgvItems>>
e o código original não usou linq e o resultado é semelhante.
Classe de teste 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 );
}
}
Resultado:
| Method | Mean | Error | StdDev |
|------- |--------:|---------:|---------:|
| First | 2.530 s | 0.0190 s | 0.0178 s |
| Second | 3.620 s | 0.0217 s | 0.0203 s |
Eu fiz outra base de teste no código original. Parece que o problema não está no 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;
}
}
Resultado:
| 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()
, a chamada para.Select( line => new DgvItems( line ) )
retorna um IEnumerable antes da chamada paraToLookup( item => item.StocksID )
. E procurar um elemento específico é pior usando IEnumerable que Array. Provavelmente, é mais rápido converter em uma matriz e executar pesquisa do que usar um ienumerable.var file = File.ReadLines( fileName );
-ReadLines
em vez deReadAllLines
e você código irá provavelmente ser mais rápidoBenchmarkDotnet
para a medição de desempenho real. Além disso, tente isolar o código real que você deseja medir e não inclua IO no teste.Respostas:
Consegui replicar o problema com o código simplificado abaixo:
É importante que os membros da tupla criada sejam cadeias de caracteres. Remover os dois
.ToString()
do código acima elimina a vantagem deToArray
. O .NET Framework se comporta um pouco diferente do .NET Core, pois basta remover apenas o primeiro.ToString()
para eliminar a diferença observada.Eu não tenho idéia do por que isso acontece.
fonte
ToArray
ouToList
força os dados a estarem na memória contígua; fazer isso forçando em um estágio específico do pipeline, mesmo que adicione custos, pode fazer com que uma operação posterior tenha menos falhas no cache do processador; falhas no cache do processador são surpreendentemente caras.