Skip to content

Commit 09ab295

Browse files
author
Nik Karpinsky
committed
Add new benchmark for reusing record type and fix boxing
- This PR adds a new benchmark that reuses the record type to reduce allocations. - This fix caches a boxed true and false value on the heap and returns the boxed value instead of reboxing each time in BooleanCovnerter. This saves on allocations and reduces memory presure in the new BenchmarkEnumerateRecordsSingleInstance benchmark.
1 parent 33970e5 commit 09ab295

File tree

4 files changed

+114
-50
lines changed

4 files changed

+114
-50
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Globalization;
3+
using System.IO;
4+
using BenchmarkDotNet.Attributes;
5+
6+
namespace CsvHelper.Benchmarks;
7+
8+
[MemoryDiagnoser]
9+
public class BenchmarkEnumerateRecordsSingleInstance
10+
{
11+
private const int entryCount = 2000;
12+
private readonly MemoryStream stream = new();
13+
14+
public class SimpleWithValueType
15+
{
16+
public int Id { get; set; }
17+
public bool Flag1 { get; set; }
18+
public bool Flag2 { get; set; }
19+
}
20+
21+
[GlobalSetup]
22+
public void GlobalSetupSimple()
23+
{
24+
using var streamWriter = new StreamWriter(this.stream, null, -1, true);
25+
using var writer = new CsvWriter(streamWriter, CultureInfo.InvariantCulture, true);
26+
var random = new Random(43); // Different seed for variety
27+
28+
writer.WriteHeader(typeof(SimpleWithValueType));
29+
writer.NextRecord();
30+
for (int i = 0; i < entryCount; ++i)
31+
{
32+
writer.WriteRecord(new SimpleWithValueType()
33+
{
34+
Id = random.Next(),
35+
Flag1 = random.Next(2) == 0,
36+
Flag2 = random.Next(2) == 0
37+
});
38+
writer.NextRecord();
39+
}
40+
}
41+
42+
[GlobalCleanup]
43+
public void GlobalCleanupSimple()
44+
{
45+
this.stream.Dispose();
46+
}
47+
48+
[Benchmark]
49+
public void EnumerateRecordsSingleInstance()
50+
{
51+
this.stream.Position = 0;
52+
using var streamReader = new StreamReader(this.stream, null, true, -1, true);
53+
using var csv = new CsvReader(streamReader, CultureInfo.InvariantCulture, true);
54+
var instance = new SimpleWithValueType();
55+
foreach (var record in csv.EnumerateRecords(instance))
56+
{
57+
_ = record;
58+
}
59+
}
60+
}

performance/CsvHelper.Benchmarks/BenchmarkMain.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ namespace CsvHelper.Benchmarks;
44

55
internal class BenchmarkMain
66
{
7-
static void Main(string[] args)
8-
{
9-
_ = BenchmarkRunner.Run<BenchmarkEnumerateRecords>();
10-
}
7+
static void Main(string[] args)
8+
{
9+
_ = BenchmarkSwitcher.FromAssembly(System.Reflection.Assembly.GetExecutingAssembly()).Run(args);
10+
}
1111
}

performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
<ItemGroup>
99
<PackageReference Include="BenchmarkDotNet" Version="0.15.0" />
10+
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.0.36421.1" />
1011
</ItemGroup>
1112

1213
<ItemGroup>

src/CsvHelper/TypeConversion/BooleanConverter.cs

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,59 +12,62 @@ namespace CsvHelper.TypeConversion;
1212
/// </summary>
1313
public class BooleanConverter : DefaultTypeConverter
1414
{
15-
/// <inheritdoc/>
16-
public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
17-
{
18-
if (bool.TryParse(text, out var b))
19-
{
20-
return b;
21-
}
15+
private static readonly object BoxedTrue = true;
16+
private static readonly object BoxedFalse = false;
2217

23-
if (short.TryParse(text, out var sh))
24-
{
25-
if (sh == 0)
26-
{
27-
return false;
28-
}
29-
if (sh == 1)
30-
{
31-
return true;
32-
}
33-
}
18+
/// <inheritdoc/>
19+
public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
20+
{
21+
if (bool.TryParse(text, out var b))
22+
{
23+
return b ? BoxedTrue : BoxedFalse;
24+
}
3425

35-
var t = (text ?? string.Empty).Trim();
36-
foreach (var trueValue in memberMapData.TypeConverterOptions.BooleanTrueValues)
37-
{
38-
if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(trueValue, t, CompareOptions.IgnoreCase) == 0)
39-
{
40-
return true;
41-
}
42-
}
26+
if (short.TryParse(text, out var sh))
27+
{
28+
if (sh == 0)
29+
{
30+
return BoxedFalse;
31+
}
32+
if (sh == 1)
33+
{
34+
return BoxedTrue;
35+
}
36+
}
4337

44-
foreach (var falseValue in memberMapData.TypeConverterOptions.BooleanFalseValues)
45-
{
46-
if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(falseValue, t, CompareOptions.IgnoreCase) == 0)
47-
{
48-
return false;
49-
}
50-
}
38+
var t = (text ?? string.Empty).Trim();
39+
foreach (var trueValue in memberMapData.TypeConverterOptions.BooleanTrueValues)
40+
{
41+
if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(trueValue, t, CompareOptions.IgnoreCase) == 0)
42+
{
43+
return BoxedTrue;
44+
}
45+
}
5146

52-
return base.ConvertFromString(text, row, memberMapData);
53-
}
47+
foreach (var falseValue in memberMapData.TypeConverterOptions.BooleanFalseValues)
48+
{
49+
if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(falseValue, t, CompareOptions.IgnoreCase) == 0)
50+
{
51+
return BoxedFalse;
52+
}
53+
}
5454

55-
/// <inheritdoc/>
56-
public override string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData)
57-
{
55+
return base.ConvertFromString(text, row, memberMapData);
56+
}
57+
58+
/// <inheritdoc/>
59+
public override string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData)
60+
{
5861
var b = value as bool?;
5962
if (b == true && memberMapData.TypeConverterOptions.BooleanTrueValues.Count > 0)
60-
{
61-
return memberMapData.TypeConverterOptions.BooleanTrueValues.First();
62-
}
63+
{
64+
return memberMapData.TypeConverterOptions.BooleanTrueValues.First();
65+
}
6366
else if (b == false && memberMapData.TypeConverterOptions.BooleanFalseValues.Count > 0)
64-
{
65-
return memberMapData.TypeConverterOptions.BooleanFalseValues.First();
66-
}
67+
{
68+
return memberMapData.TypeConverterOptions.BooleanFalseValues.First();
69+
}
6770

68-
return base.ConvertToString(value, row, memberMapData);
69-
}
71+
return base.ConvertToString(value, row, memberMapData);
72+
}
7073
}

0 commit comments

Comments
 (0)