diff --git a/.gitignore b/.gitignore index 5ef2421..aa03cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,9 @@ project.lock.json project.fragment.lock.json artifacts/ +# BenchmarkDotNet +BenchmarkDotNet.Artifacts/ + *_i.c *_p.c *_i.h diff --git a/README.md b/README.md index 1b2c60f..5e99683 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A lightweight .NET library that converts `IEnumerable` into an `IDataReader`, - **Automatic property mapping** -- public properties are discovered automatically when no explicit mapping is provided. - **Fluent column mapping API** -- choose exactly which columns to expose using expression-based or delegate-based mappings. - **Computed columns** -- map constant values or derived expressions that don't correspond to a property. -- **Multi-target** -- supports .NET 8.0, .NET 9.0, and .NET 10.0. +- **Multi-target** -- supports .NET 9.0 and .NET 10.0. ## Installation @@ -112,6 +112,33 @@ dotnet test dotnet run --project benchmarks/EnumerableDataReaderAdapter.Benchmarks -c Release ``` +### Results + +The benchmarks measure reading 10,000 rows through the `IDataReader` interface using different mapping strategies and column access patterns on .NET 10.0. + +``` +BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat) +unknown 2.10GHz, 1 CPU, 16 logical and 16 physical cores +.NET SDK 10.0.103 + [Host] : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v4 + .NET 10.0 : .NET 10.0.3 (10.0.3, 10.0.326.7603), X64 RyuJIT x86-64-v4 + +Job=.NET 10.0 Runtime=.NET 10.0 +``` + +| Method | N | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | +|----------------------------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|--------:|----------:|------------:| +| DefaultMapping_ByColumnIndex | 10000 | 1.977 ms | 0.0393 ms | 0.0623 ms | 1.00 | 0.04 | 1 | 85.9375 | 352.23 KB | 1.00 | +| MappingExpressions_ByColumnIndex | 10000 | 2.047 ms | 0.0406 ms | 0.0732 ms | 1.04 | 0.05 | 1 | 89.8438 | 366.67 KB | 1.04 | +| MappingExpressions_ByColumnIndex_CachedMapping | 10000 | 1.982 ms | 0.0385 ms | 0.0564 ms | 1.00 | 0.04 | 1 | 85.9375 | 352.29 KB | 1.00 | +| MappingDelegates_ByColumnIndex | 10000 | 1.970 ms | 0.0392 ms | 0.0575 ms | 1.00 | 0.04 | 1 | 85.9375 | 352.53 KB | 1.00 | +| MappingDelegates_ByColumnIndex_CachedMapping | 10000 | 1.969 ms | 0.0391 ms | 0.0841 ms | 1.00 | 0.05 | 1 | 85.9375 | 352.29 KB | 1.00 | +| DefaultMapping_ByColumnName | 10000 | 2.053 ms | 0.0408 ms | 0.0705 ms | 1.04 | 0.05 | 1 | 85.9375 | 352.23 KB | 1.00 | +| MappingExpressions_ByColumnName | 10000 | 2.268 ms | 0.0450 ms | 0.0776 ms | 1.15 | 0.05 | 2 | 89.8438 | 366.6 KB | 1.04 | +| MappingExpressions_ByColumnName_CachedMapping | 10000 | 2.006 ms | 0.0385 ms | 0.0804 ms | 1.02 | 0.05 | 1 | 85.9375 | 352.29 KB | 1.00 | +| MappingDelegates_ByColumnName | 10000 | 2.016 ms | 0.0397 ms | 0.0441 ms | 1.02 | 0.04 | 1 | 85.9375 | 352.53 KB | 1.00 | +| MappingDelegates_ByColumnName_CachedMapping | 10000 | 2.024 ms | 0.0389 ms | 0.0519 ms | 1.02 | 0.04 | 1 | 85.9375 | 352.37 KB | 1.00 | + ## License [MIT](LICENSE) -- Copyright (c) 2018 Dimo Terziev diff --git a/benchmarks/EnumerableDataReaderAdapter.Benchmarks/EnumerableDataReaderAdapter.Benchmarks.csproj b/benchmarks/EnumerableDataReaderAdapter.Benchmarks/EnumerableDataReaderAdapter.Benchmarks.csproj index 3150f11..169efc4 100644 --- a/benchmarks/EnumerableDataReaderAdapter.Benchmarks/EnumerableDataReaderAdapter.Benchmarks.csproj +++ b/benchmarks/EnumerableDataReaderAdapter.Benchmarks/EnumerableDataReaderAdapter.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0;net9.0;net10.0 + net9.0;net10.0 diff --git a/benchmarks/EnumerableDataReaderAdapter.Benchmarks/Program.cs b/benchmarks/EnumerableDataReaderAdapter.Benchmarks/Program.cs index 67f3b81..aef5891 100644 --- a/benchmarks/EnumerableDataReaderAdapter.Benchmarks/Program.cs +++ b/benchmarks/EnumerableDataReaderAdapter.Benchmarks/Program.cs @@ -23,7 +23,6 @@ public DataStructure( } - [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net80)] [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net90)] [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net10_0)] [RPlotExporter, RankColumn] diff --git a/src/EnumerableDataReaderAdapter/EnumerableDataReaderAdapter.csproj b/src/EnumerableDataReaderAdapter/EnumerableDataReaderAdapter.csproj index d2aec16..fda662e 100644 --- a/src/EnumerableDataReaderAdapter/EnumerableDataReaderAdapter.csproj +++ b/src/EnumerableDataReaderAdapter/EnumerableDataReaderAdapter.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + net9.0;net10.0 diff --git a/src/EnumerableDataReaderAdapter/EnumerableExtensions.cs b/src/EnumerableDataReaderAdapter/EnumerableExtensions.cs index 47cbd13..afdd04e 100644 --- a/src/EnumerableDataReaderAdapter/EnumerableExtensions.cs +++ b/src/EnumerableDataReaderAdapter/EnumerableExtensions.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Frozen; using System.Data; using System.Data.Common; using System.Runtime.CompilerServices; @@ -39,7 +40,8 @@ private sealed class EnumerableReaderAdapter : DbDataReader private bool _isClosed = false; private IEnumerator _enumerator; private T _current = default!; - private readonly Lazy> _columnLookup; + private readonly FrozenDictionary _columnLookup; + private readonly FrozenDictionary.AlternateLookup> _alternateLookup; private long _rowCount = 0; public EnumerableReaderAdapter( @@ -48,15 +50,14 @@ public EnumerableReaderAdapter( { _enumerator = rows.GetEnumerator(); _mappings = mappings; - _columnLookup = new Lazy>(() => + + var dict = new Dictionary(mappings.Length); + for (int i = 0; i < mappings.Length; i++) { - var result = new Dictionary(_mappings.Length); - for (int i = 0; i < _mappings.Length; i++) - { - result.Add(_mappings[i].ColumnName, i); - } - return result; - }); + dict.Add(mappings[i].ColumnName, i); + } + _columnLookup = dict.ToFrozenDictionary(); + _alternateLookup = _columnLookup.GetAlternateLookup>(); } public override bool HasRows => true; @@ -125,20 +126,21 @@ public override void Close() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetOrdinal(string name) => _columnLookup.Value[name]; + public override int GetOrdinal(string name) => _alternateLookup[name.AsSpan()]; [MethodImpl(MethodImplOptions.AggressiveInlining)] public override object GetValue(int i) => _mappings[i].ValueGetter(_current)!; public override int GetValues(object?[] values) { - var max = values.Length < _mappings.Length - ? values.Length - : _mappings.Length; + var max = Math.Min(values.Length, _mappings.Length); - for (int i = 0; i < max; i++) + ReadOnlySpan<(string ColumnName, Type ColumnType, Func ValueGetter)> mappingSpan = _mappings.AsSpan(0, max); + Span valuesSpan = values.AsSpan(0, max); + + for (int i = 0; i < mappingSpan.Length; i++) { - values[i] = _mappings[i].ValueGetter(_current); + valuesSpan[i] = mappingSpan[i].ValueGetter(_current); } return max; @@ -167,7 +169,14 @@ public override int GetValues(object?[] values) public override string GetString(int i) => (string)GetValue(i); public override decimal GetDecimal(int i) => (decimal)GetValue(i); public override DateTime GetDateTime(int i) => (DateTime)GetValue(i); - public override bool IsDBNull(int i) => GetValue(i) == null || GetValue(i) == DBNull.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool IsDBNull(int i) + { + var value = GetValue(i); + return value is null or DBNull; + } + public override int FieldCount => _mappings.Length; } } diff --git a/test/EnumerableDataReaderAdapter.Tests/EnumerableDataReaderAdapter.Tests.csproj b/test/EnumerableDataReaderAdapter.Tests/EnumerableDataReaderAdapter.Tests.csproj index ac0cff8..a285127 100644 --- a/test/EnumerableDataReaderAdapter.Tests/EnumerableDataReaderAdapter.Tests.csproj +++ b/test/EnumerableDataReaderAdapter.Tests/EnumerableDataReaderAdapter.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + net9.0;net10.0 false