From 09ab295ab10ea8821fcec9159c61e5e5e0a119be Mon Sep 17 00:00:00 2001 From: Nik Karpinsky Date: Wed, 20 Aug 2025 16:43:27 -0400 Subject: [PATCH] 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. --- ...BenchmarkEnumerateRecordsSingleInstance.cs | 60 ++++++++++++ .../CsvHelper.Benchmarks/BenchmarkMain.cs | 8 +- .../CsvHelper.Benchmarks.csproj | 1 + .../TypeConversion/BooleanConverter.cs | 95 ++++++++++--------- 4 files changed, 114 insertions(+), 50 deletions(-) create mode 100644 performance/CsvHelper.Benchmarks/BenchmarkEnumerateRecordsSingleInstance.cs diff --git a/performance/CsvHelper.Benchmarks/BenchmarkEnumerateRecordsSingleInstance.cs b/performance/CsvHelper.Benchmarks/BenchmarkEnumerateRecordsSingleInstance.cs new file mode 100644 index 000000000..2879b9fd8 --- /dev/null +++ b/performance/CsvHelper.Benchmarks/BenchmarkEnumerateRecordsSingleInstance.cs @@ -0,0 +1,60 @@ +using System; +using System.Globalization; +using System.IO; +using BenchmarkDotNet.Attributes; + +namespace CsvHelper.Benchmarks; + +[MemoryDiagnoser] +public class BenchmarkEnumerateRecordsSingleInstance +{ + private const int entryCount = 2000; + private readonly MemoryStream stream = new(); + + public class SimpleWithValueType + { + public int Id { get; set; } + public bool Flag1 { get; set; } + public bool Flag2 { get; set; } + } + + [GlobalSetup] + public void GlobalSetupSimple() + { + using var streamWriter = new StreamWriter(this.stream, null, -1, true); + using var writer = new CsvWriter(streamWriter, CultureInfo.InvariantCulture, true); + var random = new Random(43); // Different seed for variety + + writer.WriteHeader(typeof(SimpleWithValueType)); + writer.NextRecord(); + for (int i = 0; i < entryCount; ++i) + { + writer.WriteRecord(new SimpleWithValueType() + { + Id = random.Next(), + Flag1 = random.Next(2) == 0, + Flag2 = random.Next(2) == 0 + }); + writer.NextRecord(); + } + } + + [GlobalCleanup] + public void GlobalCleanupSimple() + { + this.stream.Dispose(); + } + + [Benchmark] + public void EnumerateRecordsSingleInstance() + { + this.stream.Position = 0; + using var streamReader = new StreamReader(this.stream, null, true, -1, true); + using var csv = new CsvReader(streamReader, CultureInfo.InvariantCulture, true); + var instance = new SimpleWithValueType(); + foreach (var record in csv.EnumerateRecords(instance)) + { + _ = record; + } + } +} diff --git a/performance/CsvHelper.Benchmarks/BenchmarkMain.cs b/performance/CsvHelper.Benchmarks/BenchmarkMain.cs index 849ffcc1b..6a0a247c9 100644 --- a/performance/CsvHelper.Benchmarks/BenchmarkMain.cs +++ b/performance/CsvHelper.Benchmarks/BenchmarkMain.cs @@ -4,8 +4,8 @@ namespace CsvHelper.Benchmarks; internal class BenchmarkMain { - static void Main(string[] args) - { - _ = BenchmarkRunner.Run(); - } + static void Main(string[] args) + { + _ = BenchmarkSwitcher.FromAssembly(System.Reflection.Assembly.GetExecutingAssembly()).Run(args); + } } diff --git a/performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj b/performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj index 248fbd13f..b3cf69f11 100644 --- a/performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj +++ b/performance/CsvHelper.Benchmarks/CsvHelper.Benchmarks.csproj @@ -7,6 +7,7 @@ + diff --git a/src/CsvHelper/TypeConversion/BooleanConverter.cs b/src/CsvHelper/TypeConversion/BooleanConverter.cs index e11e1f5fa..e30bee9e3 100644 --- a/src/CsvHelper/TypeConversion/BooleanConverter.cs +++ b/src/CsvHelper/TypeConversion/BooleanConverter.cs @@ -12,59 +12,62 @@ namespace CsvHelper.TypeConversion; /// public class BooleanConverter : DefaultTypeConverter { - /// - public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData) - { - if (bool.TryParse(text, out var b)) - { - return b; - } + private static readonly object BoxedTrue = true; + private static readonly object BoxedFalse = false; - if (short.TryParse(text, out var sh)) - { - if (sh == 0) - { - return false; - } - if (sh == 1) - { - return true; - } - } + /// + public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData) + { + if (bool.TryParse(text, out var b)) + { + return b ? BoxedTrue : BoxedFalse; + } - var t = (text ?? string.Empty).Trim(); - foreach (var trueValue in memberMapData.TypeConverterOptions.BooleanTrueValues) - { - if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(trueValue, t, CompareOptions.IgnoreCase) == 0) - { - return true; - } - } + if (short.TryParse(text, out var sh)) + { + if (sh == 0) + { + return BoxedFalse; + } + if (sh == 1) + { + return BoxedTrue; + } + } - foreach (var falseValue in memberMapData.TypeConverterOptions.BooleanFalseValues) - { - if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(falseValue, t, CompareOptions.IgnoreCase) == 0) - { - return false; - } - } + var t = (text ?? string.Empty).Trim(); + foreach (var trueValue in memberMapData.TypeConverterOptions.BooleanTrueValues) + { + if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(trueValue, t, CompareOptions.IgnoreCase) == 0) + { + return BoxedTrue; + } + } - return base.ConvertFromString(text, row, memberMapData); - } + foreach (var falseValue in memberMapData.TypeConverterOptions.BooleanFalseValues) + { + if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(falseValue, t, CompareOptions.IgnoreCase) == 0) + { + return BoxedFalse; + } + } - /// - public override string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData) - { + return base.ConvertFromString(text, row, memberMapData); + } + + /// + public override string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData) + { var b = value as bool?; if (b == true && memberMapData.TypeConverterOptions.BooleanTrueValues.Count > 0) - { - return memberMapData.TypeConverterOptions.BooleanTrueValues.First(); - } + { + return memberMapData.TypeConverterOptions.BooleanTrueValues.First(); + } else if (b == false && memberMapData.TypeConverterOptions.BooleanFalseValues.Count > 0) - { - return memberMapData.TypeConverterOptions.BooleanFalseValues.First(); - } + { + return memberMapData.TypeConverterOptions.BooleanFalseValues.First(); + } - return base.ConvertToString(value, row, memberMapData); - } + return base.ConvertToString(value, row, memberMapData); + } }