Skip to content

Commit 4fa9e8f

Browse files
committed
is custom collector to avoid intermediate array allocations
1 parent 5c7143f commit 4fa9e8f

11 files changed

Lines changed: 300 additions & 64 deletions

Dapper.StrongName/Dapper.StrongName.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919

2020
<ItemGroup Condition=" '$(TargetFramework)' == 'net461'">
2121
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
22+
<PackageReference Include="System.Memory" />
2223
</ItemGroup>
2324

2425
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
2526
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
2627
<PackageReference Include="System.Reflection.Emit.Lightweight" />
28+
<PackageReference Include="System.Memory" />
2729
</ItemGroup>
2830
</Project>

Dapper/CollectorT.cs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Collections.Generic;
4+
using System.ComponentModel;
5+
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq;
8+
using System.Runtime.CompilerServices;
9+
10+
namespace Dapper;
11+
12+
/// <summary>
13+
/// Allows efficient collection of data into lists, arrays, etc.
14+
/// </summary>
15+
/// <remarks>This is a mutable struct; treat with caution.</remarks>
16+
/// <typeparam name="T"></typeparam>
17+
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
18+
[SuppressMessage("Usage", "CA2231:Overload operator equals on overriding value type Equals", Justification = "Equality not supported")]
19+
public struct Collector<T>
20+
{
21+
/// <summary>
22+
/// Create a new collector using a size hint for the number of elements expected.
23+
/// </summary>
24+
public Collector(int capacityHint)
25+
{
26+
oversized = capacityHint > 0 ? ArrayPool<T>.Shared.Rent(capacityHint) : [];
27+
capacity = oversized.Length;
28+
}
29+
30+
/// <inheritdoc/>
31+
public readonly override string ToString() => $"Count: {count}";
32+
33+
/// <inheritdoc/>
34+
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
35+
public readonly override bool Equals([NotNullWhen(true)] object? obj) => throw new NotSupportedException();
36+
37+
/// <inheritdoc/>
38+
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
39+
public readonly override int GetHashCode() => throw new NotSupportedException();
40+
41+
private T[] oversized;
42+
private int count, capacity;
43+
44+
/// <summary>
45+
/// Gets the current capacity of the backing buffer of this instance.
46+
/// </summary>
47+
internal readonly int Capacity => capacity;
48+
49+
/// <summary>
50+
/// Gets the number of elements represented by this instance.
51+
/// </summary>
52+
public readonly int Count => count;
53+
54+
/// <summary>
55+
/// Gets the underlying elements represented by this instance.
56+
/// </summary>
57+
public readonly Span<T> Span => new(oversized, 0, count);
58+
59+
/// <summary>
60+
/// Gets the underlying elements represented by this instance.
61+
/// </summary>
62+
public readonly ArraySegment<T> ArraySegment => new(oversized, 0, count);
63+
64+
/// <summary>
65+
/// Gets the element at the specified index.
66+
/// </summary>
67+
public readonly ref T this[int index]
68+
{
69+
get
70+
{
71+
return ref index >= 0 & index < count ? ref oversized[index] : ref OutOfRange();
72+
73+
static ref T OutOfRange() => throw new ArgumentOutOfRangeException(nameof(index));
74+
}
75+
}
76+
77+
/// <summary>
78+
/// Add an element to the collection.
79+
/// </summary>
80+
public void Add(T value)
81+
{
82+
if (capacity == count) Expand();
83+
oversized[count++] = value;
84+
}
85+
86+
/// <summary>
87+
/// Add elements to the collection.
88+
/// </summary>
89+
public void AddRange(ReadOnlySpan<T> values)
90+
{
91+
EnsureCapacity(count + values.Length);
92+
values.CopyTo(new(oversized, count, values.Length));
93+
count += values.Length;
94+
}
95+
96+
private void EnsureCapacity(int minCapacity)
97+
{
98+
if (capacity < minCapacity)
99+
{
100+
var newBuffer = ArrayPool<T>.Shared.Rent(minCapacity);
101+
Span.CopyTo(newBuffer);
102+
var oldBuffer = oversized;
103+
oversized = newBuffer;
104+
capacity = newBuffer.Length;
105+
106+
if (oldBuffer is not null)
107+
{
108+
ArrayPool<T>.Shared.Return(oldBuffer);
109+
}
110+
}
111+
}
112+
113+
[MethodImpl(MethodImplOptions.NoInlining)]
114+
private void Expand() => EnsureCapacity(Math.Max(capacity * 2, 16));
115+
116+
/// <summary>
117+
/// Release any resources associated with this instance.
118+
/// </summary>
119+
public void Clear()
120+
{
121+
count = 0;
122+
if (capacity != 0)
123+
{
124+
capacity = 0;
125+
ArrayPool<T>.Shared.Return(oversized);
126+
oversized = [];
127+
}
128+
}
129+
130+
/// <summary>
131+
/// Create an array with the elements associated with this instance, and release any resources.
132+
/// </summary>
133+
public T[] ToArrayAndClear()
134+
{
135+
T[] result = [.. Span]; // let the compiler worry about the per-platform implementation
136+
Clear();
137+
return result;
138+
}
139+
140+
/// <summary>
141+
/// Create an array with the elements associated with this instance, and release any resources.
142+
/// </summary>
143+
public List<T> ToListAndClear()
144+
{
145+
List<T> result = [.. Span]; // let the compiler worry about the per-platform implementation (net8+ in particular)
146+
Clear();
147+
return result;
148+
}
149+
}

Dapper/Dapper.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626

2727
<ItemGroup Condition=" '$(TargetFramework)' == 'net461'">
2828
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
29+
<PackageReference Include="System.Memory" />
2930
</ItemGroup>
3031

3132
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
3233
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
3334
<PackageReference Include="System.Reflection.Emit.Lightweight" />
35+
<PackageReference Include="System.Memory" />
3436
</ItemGroup>
3537
</Project>

Dapper/DefaultTypeMap.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ internal static List<PropertyInfo> GetSettableProps(Type t)
5151
return t
5252
.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
5353
.Where(p => GetPropertySetter(p, t) is not null)
54-
.ToList();
54+
.AsList();
5555
}
5656

5757
internal static List<FieldInfo> GetSettableFields(Type t)
5858
{
59-
return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList();
59+
return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).AsList();
6060
}
6161

6262
/// <summary>
@@ -115,7 +115,7 @@ internal static List<FieldInfo> GetSettableFields(Type t)
115115
public ConstructorInfo? FindExplicitConstructor()
116116
{
117117
var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
118-
var withAttr = constructors.Where(c => c.GetCustomAttributes(typeof(ExplicitConstructorAttribute), true).Length > 0).ToList();
118+
var withAttr = constructors.Where(c => c.GetCustomAttributes(typeof(ExplicitConstructorAttribute), true).Length > 0).AsList();
119119

120120
if (withAttr.Count == 1)
121121
{

Dapper/PublicAPI.Shipped.txt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ static Dapper.SqlMapper.AddTypeHandlerImpl(System.Type! type, Dapper.SqlMapper.I
177177
static Dapper.SqlMapper.AddTypeMap(System.Type! type, System.Data.DbType dbType) -> void
178178
static Dapper.SqlMapper.AddTypeMap(System.Type! type, System.Data.DbType dbType, bool useGetFieldValue) -> void
179179
static Dapper.SqlMapper.AsList<T>(this System.Collections.Generic.IEnumerable<T>? source) -> System.Collections.Generic.List<T>!
180+
static Dapper.SqlMapper.AsListAsync<T>(this System.Collections.Generic.IAsyncEnumerable<T>? source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.List<T>!>!
180181
static Dapper.SqlMapper.AsTableValuedParameter(this System.Data.DataTable! table, string? typeName = null) -> Dapper.SqlMapper.ICustomQueryParameter!
181182
static Dapper.SqlMapper.AsTableValuedParameter<T>(this System.Collections.Generic.IEnumerable<T>! list, string? typeName = null) -> Dapper.SqlMapper.ICustomQueryParameter!
182183
static Dapper.SqlMapper.ConnectionStringComparer.get -> System.Collections.Generic.IEqualityComparer<string!>!
@@ -332,4 +333,20 @@ static Dapper.SqlMapper.ThrowDataException(System.Exception! ex, int index, Syst
332333
static Dapper.SqlMapper.ThrowNullCustomQueryParameter(string! name) -> void
333334
static Dapper.SqlMapper.TypeHandlerCache<T>.Parse(object! value) -> T?
334335
static Dapper.SqlMapper.TypeHandlerCache<T>.SetValue(System.Data.IDbDataParameter! parameter, object! value) -> void
335-
static Dapper.SqlMapper.TypeMapProvider -> System.Func<System.Type!, Dapper.SqlMapper.ITypeMap!>!
336+
static Dapper.SqlMapper.TypeMapProvider -> System.Func<System.Type!, Dapper.SqlMapper.ITypeMap!>!
337+
338+
Dapper.Collector<T>
339+
Dapper.Collector<T>.Collector() -> void
340+
Dapper.Collector<T>.Collector(int capacityHint) -> void
341+
Dapper.Collector<T>.Count.get -> int
342+
Dapper.Collector<T>.Span.get -> System.Span<T>
343+
Dapper.Collector<T>.ArraySegment.get -> System.ArraySegment<T>
344+
Dapper.Collector<T>.Clear() -> void
345+
Dapper.Collector<T>.Add(T value) -> void
346+
Dapper.Collector<T>.AddRange(System.ReadOnlySpan<T> values) -> void
347+
Dapper.Collector<T>.ToListAndClear() -> System.Collections.Generic.List<T>!
348+
Dapper.Collector<T>.ToArrayAndClear() -> T[]!
349+
Dapper.Collector<T>.this[int index].get -> T
350+
override Dapper.Collector<T>.ToString() -> string!
351+
override Dapper.Collector<T>.GetHashCode() -> int
352+
override Dapper.Collector<T>.Equals(object? obj) -> bool

Dapper/SqlMapper.Async.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ private static async Task<IEnumerable<T>> QueryAsync<T>(this IDbConnection cnn,
447447

448448
if (command.Buffered)
449449
{
450-
var buffer = new List<T>();
450+
var buffer = new Collector<T>();
451451
var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
452452
while (await reader.ReadAsync(cancel).ConfigureAwait(false))
453453
{
@@ -456,7 +456,7 @@ private static async Task<IEnumerable<T>> QueryAsync<T>(this IDbConnection cnn,
456456
}
457457
while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ }
458458
command.OnCompleted();
459-
return buffer;
459+
return buffer.ToListAndClear();
460460
}
461461
else
462462
{
@@ -546,6 +546,32 @@ public static Task<int> ExecuteAsync(this IDbConnection cnn, CommandDefinition c
546546
}
547547
}
548548

549+
/// <summary>
550+
/// Asynchronously collect a sequence of data into a list.
551+
/// </summary>
552+
/// <typeparam name="T">The type of element in the list.</typeparam>
553+
/// <param name="source">The enumerable to return as a list.</param>
554+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
555+
public static Task<List<T>> AsListAsync<T>(this IAsyncEnumerable<T>? source, CancellationToken cancellationToken = default)
556+
{
557+
if (source is null) return null!; // GIGO
558+
559+
return EnumerateAsync(source, cancellationToken);
560+
561+
static async Task<List<T>> EnumerateAsync(IAsyncEnumerable<T> source, CancellationToken cancellationToken)
562+
{
563+
var buffer = new Collector<T>(); // amortizes intermediate buffers
564+
await using (var iterator = source.GetAsyncEnumerator(cancellationToken))
565+
{
566+
while (await iterator.MoveNextAsync().ConfigureAwait(false))
567+
{
568+
buffer.Add(iterator.Current);
569+
}
570+
}
571+
return buffer.ToListAndClear();
572+
}
573+
}
574+
549575
private readonly struct AsyncExecState
550576
{
551577
public readonly DbCommand Command;
@@ -941,7 +967,7 @@ private static async Task<IEnumerable<TReturn>> MultiMapAsync<TFirst, TSecond, T
941967
using var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false);
942968
if (!command.Buffered) wasClosed = false; // handing back open reader; rely on command-behavior
943969
var results = MultiMapImpl<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(null, CommandDefinition.ForCallback(command.Parameters, command.Flags), map, splitOn, reader, identity, true);
944-
return command.Buffered ? results.ToList() : results;
970+
return command.Buffered ? results.AsList() : results;
945971
}
946972
finally
947973
{
@@ -989,7 +1015,7 @@ private static async Task<IEnumerable<TReturn>> MultiMapAsync<TReturn>(this IDbC
9891015
using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader);
9901016
using var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false);
9911017
var results = MultiMapImpl(null, default, types, map, splitOn, reader, identity, true);
992-
return command.Buffered ? results.ToList() : results;
1018+
return command.Buffered ? results.AsList() : results;
9931019
}
9941020
finally
9951021
{

Dapper/SqlMapper.GridReader.Async.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,12 @@ private async Task<IEnumerable<T>> ReadBufferedAsync<T>(int index, Func<DbDataRe
229229
{
230230
try
231231
{
232-
var buffer = new List<T>();
232+
var buffer = new Collector<T>();
233233
while (index == ResultIndex && await reader!.ReadAsync(cancel).ConfigureAwait(false))
234234
{
235235
buffer.Add(ConvertTo<T>(deserializer(reader)));
236236
}
237-
return buffer;
237+
return buffer.ToListAndClear();
238238
}
239239
finally // finally so that First etc progresses things even when multiple rows
240240
{

Dapper/SqlMapper.GridReader.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ private IEnumerable<T> ReadImpl<T>(Type type, bool buffered)
202202
cache.Deserializer = deserializer;
203203
}
204204
var result = ReadDeferred<T>(index, deserializer.Func, type);
205-
return buffered ? result.ToList() : result;
205+
return buffered ? result.AsList() : result;
206206
}
207207

208208
private T ReadRow<T>(Type type, Row row)
@@ -283,7 +283,7 @@ private IEnumerable<TReturn> MultiReadInternal<TReturn>(Type[] types, Func<objec
283283
public IEnumerable<TReturn> Read<TFirst, TSecond, TReturn>(Func<TFirst, TSecond, TReturn> func, string splitOn = "id", bool buffered = true)
284284
{
285285
var result = MultiReadInternal<TFirst, TSecond, DontMap, DontMap, DontMap, DontMap, DontMap, TReturn>(func, splitOn);
286-
return buffered ? result.ToList() : result;
286+
return buffered ? result.AsList() : result;
287287
}
288288

289289
/// <summary>
@@ -299,7 +299,7 @@ public IEnumerable<TReturn> Read<TFirst, TSecond, TReturn>(Func<TFirst, TSecond,
299299
public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TReturn>(Func<TFirst, TSecond, TThird, TReturn> func, string splitOn = "id", bool buffered = true)
300300
{
301301
var result = MultiReadInternal<TFirst, TSecond, TThird, DontMap, DontMap, DontMap, DontMap, TReturn>(func, splitOn);
302-
return buffered ? result.ToList() : result;
302+
return buffered ? result.AsList() : result;
303303
}
304304

305305
/// <summary>
@@ -316,7 +316,7 @@ public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TReturn>(Func<TFirst,
316316
public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TReturn>(Func<TFirst, TSecond, TThird, TFourth, TReturn> func, string splitOn = "id", bool buffered = true)
317317
{
318318
var result = MultiReadInternal<TFirst, TSecond, TThird, TFourth, DontMap, DontMap, DontMap, TReturn>(func, splitOn);
319-
return buffered ? result.ToList() : result;
319+
return buffered ? result.AsList() : result;
320320
}
321321

322322
/// <summary>
@@ -334,7 +334,7 @@ public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TReturn>(Func
334334
public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(Func<TFirst, TSecond, TThird, TFourth, TFifth, TReturn> func, string splitOn = "id", bool buffered = true)
335335
{
336336
var result = MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, DontMap, DontMap, TReturn>(func, splitOn);
337-
return buffered ? result.ToList() : result;
337+
return buffered ? result.AsList() : result;
338338
}
339339

340340
/// <summary>
@@ -353,7 +353,7 @@ public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TFifth, TRetu
353353
public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TReturn>(Func<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TReturn> func, string splitOn = "id", bool buffered = true)
354354
{
355355
var result = MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, DontMap, TReturn>(func, splitOn);
356-
return buffered ? result.ToList() : result;
356+
return buffered ? result.AsList() : result;
357357
}
358358

359359
/// <summary>
@@ -373,7 +373,7 @@ public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TFifth, TSixt
373373
public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(Func<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn> func, string splitOn = "id", bool buffered = true)
374374
{
375375
var result = MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(func, splitOn);
376-
return buffered ? result.ToList() : result;
376+
return buffered ? result.AsList() : result;
377377
}
378378

379379
/// <summary>
@@ -387,7 +387,7 @@ public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TFifth, TSixt
387387
public IEnumerable<TReturn> Read<TReturn>(Type[] types, Func<object[], TReturn> map, string splitOn = "id", bool buffered = true)
388388
{
389389
var result = MultiReadInternal(types, map, splitOn);
390-
return buffered ? result.ToList() : result;
390+
return buffered ? result.AsList() : result;
391391
}
392392

393393
private IEnumerable<T> ReadDeferred<T>(int index, Func<DbDataReader, object> deserializer, Type effectiveType)

0 commit comments

Comments
 (0)