Skip to content

Commit

Permalink
Improved GetDynamicRecords performance (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
shibayan authored Oct 9, 2022
1 parent 550c29e commit a7fe921
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
</ItemGroup>

<ItemGroup>
Expand Down
41 changes: 40 additions & 1 deletion CsvHelper.FastDynamic.Performance/Internal/CsvReaderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal static IReadOnlyList<IDictionary<string, object>> GetDictionaryRecords(

internal static IEnumerable<IDictionary<string, object>> EnumerateDictionaryRecords(this CsvReader csvReader)
{
if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord == null)
if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord is null)
{
if (!csvReader.Read())
{
Expand Down Expand Up @@ -53,4 +53,43 @@ internal static IEnumerable<IDictionary<string, object>> EnumerateDictionaryReco
yield return record;
}
}

internal static IReadOnlyList<string[]> GetRawRecords(this CsvReader csvReader)
=> csvReader.EnumerateRawRecords().ToArray();

internal static IEnumerable<string[]> EnumerateRawRecords(this CsvReader csvReader)
{
if (csvReader.Configuration.HasHeaderRecord && csvReader.HeaderRecord is null)
{
if (!csvReader.Read())
{
yield break;
}

csvReader.ReadHeader();
}

while (csvReader.Read())
{
string[] record;

try
{
record = csvReader.Parser.Record;
}
catch (Exception ex)
{
var readerException = new ReaderException(csvReader.Context, "An unexpected error occurred.", ex);

if (csvReader.Configuration.ReadingExceptionOccurred?.Invoke(new ReadingExceptionOccurredArgs(readerException)) ?? true)
{
throw readerException;
}

continue;
}

yield return record;
}
}
}
10 changes: 9 additions & 1 deletion CsvHelper.FastDynamic.Performance/ReaderBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public ReaderBenchmark()

private readonly string _sampleCsvData;

[Benchmark]
[Benchmark(Baseline = true)]
public IReadOnlyList<dynamic> GetRecords()
{
using var csvReader = new CsvReader(new StringReader(_sampleCsvData), CultureInfo.InvariantCulture);
Expand All @@ -44,4 +44,12 @@ public IReadOnlyList<dynamic> GetDynamicRecords()

return csvReader.GetDynamicRecords();
}

[Benchmark]
public IReadOnlyList<string[]> GetRawRecords()
{
using var csvReader = new CsvReader(new StringReader(_sampleCsvData), CultureInfo.InvariantCulture);

return csvReader.GetRawRecords();
}
}
2 changes: 1 addition & 1 deletion CsvHelper.FastDynamic.Performance/WriterBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public WriterBenchmark()

private readonly IReadOnlyList<dynamic> _dynamicCsvData;

[Benchmark]
[Benchmark(Baseline = true)]
public void WriteRecords_DynamicObject()
{
using var csvWriter = new CsvWriter(new StringWriter(), CultureInfo.InvariantCulture);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
50 changes: 44 additions & 6 deletions CsvHelper.FastDynamic.Tests/CsvReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace CsvHelper.FastDynamic.Tests;
public class CsvReaderTests
{
[Fact]
public void ReadDynamicRecords()
public void GetDynamicRecords()
{
var csvReader = CreateInMemoryReader();

Expand All @@ -29,7 +29,7 @@ public void ReadDynamicRecords()
}

[Fact]
public void ReadDynamicRecords_UseIndexer()
public void GetDynamicRecords_UseIndexer()
{
var csvReader = CreateInMemoryReader();

Expand All @@ -47,7 +47,7 @@ public void ReadDynamicRecords_UseIndexer()
}

[Fact]
public void ReadDynamicRecords_AsDictionary()
public void GetDynamicRecords_AsDictionary()
{
var csvReader = CreateInMemoryReader();

Expand All @@ -67,7 +67,7 @@ public void ReadDynamicRecords_AsDictionary()
}

[Fact]
public void ReadDynamicRecords_WithMissingHeader()
public void GetDynamicRecords_WithMissingHeader()
{
var csvReader = CreateInMemoryReader_WithMissingHeader();

Expand All @@ -88,7 +88,7 @@ public void ReadDynamicRecords_WithMissingHeader()
}

[Fact]
public async Task ReadDynamicRecordsAsync()
public async Task GetDynamicRecordsAsync()
{
var csvReader = CreateInMemoryReader();

Expand All @@ -106,7 +106,45 @@ public async Task ReadDynamicRecordsAsync()
}

[Fact]
public async Task ReadDynamicRecordsAsync_WithMissingHeader()
public async Task GetDynamicRecordsAsync_UseIndexer()
{
var csvReader = CreateInMemoryReader();

var records = await csvReader.GetDynamicRecordsAsync();

Assert.NotNull(records);
Assert.Equal(3, records.Count);

for (var i = 0; i < 3; i++)
{
Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]);
Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]);
Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]);
}
}

[Fact]
public async Task GetDynamicRecordsAsync_AsDictionary()
{
var csvReader = CreateInMemoryReader();

var records = (await csvReader.GetDynamicRecordsAsync())
.Cast<IDictionary<string, object>>()
.ToArray();

Assert.NotNull(records);
Assert.Equal(3, records.Length);

for (var i = 0; i < 3; i++)
{
Assert.Equal(TestData.CsvRecords[i]["Id"], records[i]["Id"]);
Assert.Equal(TestData.CsvRecords[i]["Name"], records[i]["Name"]);
Assert.Equal(TestData.CsvRecords[i]["Location"], records[i]["Location"]);
}
}

[Fact]
public async Task GetDynamicRecordsAsync_WithMissingHeader()
{
var csvReader = CreateInMemoryReader_WithMissingHeader();

Expand Down
2 changes: 1 addition & 1 deletion CsvHelper.FastDynamic/CsvHelper.FastDynamic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.2.1" />
<PackageReference Include="CsvHelper" Version="29.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
10 changes: 8 additions & 2 deletions CsvHelper.FastDynamic/CsvReaderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ public static IEnumerable<dynamic> EnumerateDynamicRecords(this CsvReader csvRea
{
var values = new object[csvReader.HeaderRecord.Length];

Array.Copy(csvReader.Parser.Record, values, csvReader.HeaderRecord.Length);
for (var i = 0; i < csvReader.HeaderRecord.Length; i++)
{
values[i] = csvReader.Parser[i];
}

record = new CsvRecord(csvHeader, values);
}
Expand Down Expand Up @@ -91,7 +94,10 @@ public static async IAsyncEnumerable<dynamic> EnumerateDynamicRecordsAsync(this
{
var values = new object[csvReader.HeaderRecord.Length];

Array.Copy(csvReader.Parser.Record, values, csvReader.HeaderRecord.Length);
for (var i = 0; i < csvReader.HeaderRecord.Length; i++)
{
values[i] = csvReader.Parser[i];
}

record = new CsvRecord(csvHeader, values);
}
Expand Down
5 changes: 1 addition & 4 deletions CsvHelper.FastDynamic/CsvRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@ object IDictionary<string, object>.this[string key]

int IReadOnlyCollection<KeyValuePair<string, object>>.Count => _values.Count(x => x is not DeadValue);

object IReadOnlyDictionary<string, object>.this[string key]
{
get => TryGetValue(key, out var value) ? value : null;
}
object IReadOnlyDictionary<string, object>.this[string key] => TryGetValue(key, out var value) ? value : null;

bool IReadOnlyDictionary<string, object>.TryGetValue(string key, out object value) => TryGetValue(key, out value);

Expand Down
37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ foreach (var @record in records)
}
```

### Async CSV Enumerate (.NET Standard 2.1 / C# 8.0)
### Async CSV Enumerate (.NET Standard 2.1 / C# 8.0 or later)

```csharp
using CsvHelper;
Expand All @@ -56,34 +56,35 @@ await foreach (var @record in records)
### Dynamic record reader

```
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.608)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.200
[Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
.NET SDK=6.0.401
[Host] : .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
|--------------------- |---------:|--------:|--------:|--------:|--------:|----------:|
| GetRecords | 834.0 us | 3.04 us | 2.85 us | 36.1328 | 17.5781 | 602 KB |
| GetDictionaryRecords | 247.4 us | 0.89 us | 0.78 us | 32.7148 | 16.1133 | 538 KB |
| GetDynamicRecords | 227.5 us | 0.85 us | 0.80 us | 28.3203 | 13.4277 | 464 KB |
| Method | Mean | Error | StdDev | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio |
|--------------------- |---------:|--------:|--------:|------:|--------:|--------:|----------:|------------:|
| GetRecords | 878.3 us | 3.62 us | 3.20 us | 1.00 | 31.2500 | 15.6250 | 510.84 KB | 1.00 |
| GetDictionaryRecords | 208.0 us | 0.85 us | 0.76 us | 0.24 | 21.7285 | 10.7422 | 355.03 KB | 0.69 |
| GetDynamicRecords | 176.4 us | 1.07 us | 0.95 us | 0.20 | 14.4043 | 6.3477 | 237.26 KB | 0.46 |
| GetRawRecords | 154.7 us | 1.08 us | 1.01 us | 0.18 | 13.1836 | 5.8594 | 218.98 KB | 0.43 |
```

### Dynamic record writer

```
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.608)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.200
[Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
.NET SDK=6.0.401
[Host] : .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
|---------------------------------- |---------:|--------:|--------:|--------:|-------:|----------:|
| WriteRecords_DynamicObject | 819.2 us | 0.85 us | 0.76 us | 49.8047 | 9.7656 | 822 KB |
| WriteDynamicRecords_DynamicObject | 455.0 us | 2.22 us | 1.97 us | 7.8125 | 1.4648 | 134 KB |
| Method | Mean | Error | StdDev | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---------------------------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|------------:|
| WriteRecords_DynamicObject | 873.9 us | 2.82 us | 2.64 us | 1.00 | 55.6641 | 9.7656 | 914.53 KB | 1.00 |
| WriteDynamicRecords_DynamicObject | 498.9 us | 2.52 us | 2.36 us | 0.57 | 13.6719 | 2.4414 | 225.84 KB | 0.25 |
```

## Thanks
Expand Down

0 comments on commit a7fe921

Please sign in to comment.