Skip to content

Commit dc7e80b

Browse files
authored
Optimize QueryCollection (#32829)
1 parent 0aa82c3 commit dc7e80b

File tree

7 files changed

+556
-30
lines changed

7 files changed

+556
-30
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using BenchmarkDotNet.Configs;
6+
using Microsoft.AspNetCore.Http.Features;
7+
using Microsoft.AspNetCore.WebUtilities;
8+
using static Microsoft.AspNetCore.Http.Features.QueryFeature;
9+
10+
namespace Microsoft.AspNetCore.Http
11+
{
12+
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
13+
[CategoriesColumn]
14+
public class QueryCollectionBenchmarks
15+
{
16+
private string _queryString;
17+
private string _singleValue;
18+
private string _singleValueWithPlus;
19+
private string _encoded;
20+
21+
[IterationSetup]
22+
public void Setup()
23+
{
24+
_queryString = "?key1=value1&key2=value2&key3=value3&key4=&key5=";
25+
_singleValue = "?key1=value1";
26+
_singleValueWithPlus = "?key1=value1+value2+value3";
27+
_encoded = "?key1=value%231";
28+
}
29+
30+
[Benchmark(Description = "ParseNew")]
31+
[BenchmarkCategory("QueryString")]
32+
public void ParseNew()
33+
{
34+
_ = QueryFeature.ParseNullableQueryInternal(_queryString);
35+
}
36+
37+
[Benchmark(Description = "ParseNew")]
38+
[BenchmarkCategory("Single")]
39+
public void ParseNewSingle()
40+
{
41+
_ = QueryFeature.ParseNullableQueryInternal(_singleValue);
42+
}
43+
44+
[Benchmark(Description = "ParseNew")]
45+
[BenchmarkCategory("SingleWithPlus")]
46+
public void ParseNewSingleWithPlus()
47+
{
48+
_ = QueryFeature.ParseNullableQueryInternal(_singleValueWithPlus);
49+
}
50+
51+
[Benchmark(Description = "ParseNew")]
52+
[BenchmarkCategory("Encoded")]
53+
public void ParseNewEncoded()
54+
{
55+
_ = QueryFeature.ParseNullableQueryInternal(_encoded);
56+
}
57+
58+
[Benchmark(Description = "QueryHelpersParse")]
59+
[BenchmarkCategory("QueryString")]
60+
public void QueryHelpersParse()
61+
{
62+
_ = QueryHelpers.ParseNullableQuery(_queryString);
63+
}
64+
65+
[Benchmark(Description = "QueryHelpersParse")]
66+
[BenchmarkCategory("Single")]
67+
public void QueryHelpersParseSingle()
68+
{
69+
_ = QueryHelpers.ParseNullableQuery(_singleValue);
70+
}
71+
72+
[Benchmark(Description = "QueryHelpersParse")]
73+
[BenchmarkCategory("SingleWithPlus")]
74+
public void QueryHelpersParseSingleWithPlus()
75+
{
76+
_ = QueryHelpers.ParseNullableQuery(_singleValueWithPlus);
77+
}
78+
79+
[Benchmark(Description = "QueryHelpersParse")]
80+
[BenchmarkCategory("Encoded")]
81+
public void QueryHelpersParseEncoded()
82+
{
83+
_ = QueryHelpers.ParseNullableQuery(_encoded);
84+
}
85+
86+
[Benchmark]
87+
[BenchmarkCategory("Constructor")]
88+
public void Constructor()
89+
{
90+
var dict = new KvpAccumulator();
91+
if (dict.HasValues)
92+
{
93+
return;
94+
}
95+
}
96+
}
97+
}

src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public void Setup()
1919
[Benchmark]
2020
public void Parse_TypicalCookie()
2121
{
22-
RequestCookieCollection.Parse(_cookie);
22+
_ = RequestCookieCollection.Parse(_cookie);
2323
}
2424
}
2525
}

src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ public void Ctor_Values_RouteValueDictionary_EmptyArray()
3333
}
3434

3535
[Benchmark]
36-
public void Ctor_Values_RouteValueDictionary_Array()
36+
public RouteValueDictionary Ctor_Values_RouteValueDictionary_Array()
3737
{
38-
new RouteValueDictionary(_arrayValues);
38+
return new RouteValueDictionary(_arrayValues);
3939
}
4040

4141
[Benchmark]

src/Http/Http/src/Features/QueryFeature.cs

Lines changed: 230 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using Microsoft.AspNetCore.WebUtilities;
5+
using System.Buffers;
6+
using System.Collections.Generic;
7+
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
9+
using System.Runtime.Intrinsics;
10+
using System.Runtime.Intrinsics.X86;
11+
using Microsoft.AspNetCore.Internal;
12+
using Microsoft.Extensions.Primitives;
613

714
namespace Microsoft.AspNetCore.Http.Features
815
{
@@ -55,30 +62,21 @@ public IQueryCollection Query
5562
{
5663
get
5764
{
58-
if (_features.Collection == null)
65+
if (_features.Collection is null)
5966
{
60-
if (_parsedValues == null)
61-
{
62-
_parsedValues = QueryCollection.Empty;
63-
}
64-
return _parsedValues;
67+
return _parsedValues ?? QueryCollection.Empty;
6568
}
6669

6770
var current = HttpRequestFeature.QueryString;
6871
if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal))
6972
{
7073
_original = current;
7174

72-
var result = QueryHelpers.ParseNullableQuery(current);
75+
var result = ParseNullableQueryInternal(current);
7376

74-
if (result == null)
75-
{
76-
_parsedValues = QueryCollection.Empty;
77-
}
78-
else
79-
{
80-
_parsedValues = new QueryCollection(result);
81-
}
77+
_parsedValues = result is not null
78+
? new QueryCollectionInternal(result)
79+
: QueryCollection.Empty;
8280
}
8381
return _parsedValues;
8482
}
@@ -100,5 +98,221 @@ public IQueryCollection Query
10098
}
10199
}
102100
}
101+
102+
/// <summary>
103+
/// Parse a query string into its component key and value parts.
104+
/// </summary>
105+
/// <param name="queryString">The raw query string value, with or without the leading '?'.</param>
106+
/// <returns>A collection of parsed keys and values, null if there are no entries.</returns>
107+
[SkipLocalsInit]
108+
internal static AdaptiveCapacityDictionary<string, StringValues>? ParseNullableQueryInternal(string? queryString)
109+
{
110+
if (string.IsNullOrEmpty(queryString) || (queryString.Length == 1 && queryString[0] == '?'))
111+
{
112+
return null;
113+
}
114+
115+
var accumulator = new KvpAccumulator();
116+
var query = queryString.AsSpan();
117+
118+
if (query[0] == '?')
119+
{
120+
query = query[1..];
121+
}
122+
123+
while (!query.IsEmpty)
124+
{
125+
var delimiterIndex = query.IndexOf('&');
126+
127+
var querySegment = delimiterIndex >= 0
128+
? query.Slice(0, delimiterIndex)
129+
: query;
130+
131+
var equalIndex = querySegment.IndexOf('=');
132+
133+
if (equalIndex >= 0)
134+
{
135+
var name = SpanHelper.ReplacePlusWithSpace(querySegment.Slice(0, equalIndex));
136+
var value = SpanHelper.ReplacePlusWithSpace(querySegment.Slice(equalIndex + 1));
137+
138+
accumulator.Append(
139+
Uri.UnescapeDataString(name),
140+
Uri.UnescapeDataString(value));
141+
}
142+
else
143+
{
144+
if (!querySegment.IsEmpty)
145+
{
146+
accumulator.Append(querySegment);
147+
}
148+
}
149+
150+
if (delimiterIndex < 0)
151+
{
152+
break;
153+
}
154+
155+
query = query.Slice(delimiterIndex + 1);
156+
}
157+
158+
return accumulator.HasValues
159+
? accumulator.GetResults()
160+
: null;
161+
}
162+
163+
internal struct KvpAccumulator
164+
{
165+
/// <summary>
166+
/// This API supports infrastructure and is not intended to be used
167+
/// directly from your code. This API may change or be removed in future releases.
168+
/// </summary>
169+
private AdaptiveCapacityDictionary<string, StringValues> _accumulator;
170+
private AdaptiveCapacityDictionary<string, List<string>> _expandingAccumulator;
171+
172+
public void Append(ReadOnlySpan<char> key, ReadOnlySpan<char> value = default)
173+
=> Append(key.ToString(), value.IsEmpty ? string.Empty : value.ToString());
174+
175+
/// <summary>
176+
/// This API supports infrastructure and is not intended to be used
177+
/// directly from your code. This API may change or be removed in future releases.
178+
/// </summary>
179+
public void Append(string key, string value)
180+
{
181+
if (_accumulator is null)
182+
{
183+
_accumulator = new AdaptiveCapacityDictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
184+
}
185+
186+
if (!_accumulator.TryGetValue(key, out var values))
187+
{
188+
// First value for this key
189+
_accumulator[key] = new StringValues(value);
190+
}
191+
else
192+
{
193+
AppendToExpandingAccumulator(key, value, values);
194+
}
195+
196+
ValueCount++;
197+
}
198+
199+
private void AppendToExpandingAccumulator(string key, string value, StringValues values)
200+
{
201+
// When there are some values for the same key, so switch to expanding accumulator, and
202+
// add a zero count marker in the accumulator to indicate that switch.
203+
204+
if (values.Count != 0)
205+
{
206+
_accumulator[key] = default;
207+
208+
if (_expandingAccumulator is null)
209+
{
210+
_expandingAccumulator = new AdaptiveCapacityDictionary<string, List<string>>(capacity: 5, StringComparer.OrdinalIgnoreCase);
211+
}
212+
213+
// Already 2 (1 existing + the new one) entries so use List's expansion mechanism for more
214+
var list = new List<string>();
215+
216+
list.AddRange(values);
217+
list.Add(value);
218+
219+
_expandingAccumulator[key] = list;
220+
}
221+
else
222+
{
223+
// The marker indicates we are in the expanding accumulator, so just append to the list.
224+
_expandingAccumulator[key].Add(value);
225+
}
226+
}
227+
228+
/// <summary>
229+
/// This API supports infrastructure and is not intended to be used
230+
/// directly from your code. This API may change or be removed in future releases.
231+
/// </summary>
232+
public bool HasValues => ValueCount > 0;
233+
234+
/// <summary>
235+
/// This API supports infrastructure and is not intended to be used
236+
/// directly from your code. This API may change or be removed in future releases.
237+
/// </summary>
238+
public int KeyCount => _accumulator?.Count ?? 0;
239+
240+
/// <summary>
241+
/// This API supports infrastructure and is not intended to be used
242+
/// directly from your code. This API may change or be removed in future releases.
243+
/// </summary>
244+
public int ValueCount { get; private set; }
245+
246+
/// <summary>
247+
/// This API supports infrastructure and is not intended to be used
248+
/// directly from your code. This API may change or be removed in future releases.
249+
/// </summary>
250+
public AdaptiveCapacityDictionary<string, StringValues> GetResults()
251+
{
252+
if (_expandingAccumulator != null)
253+
{
254+
// Coalesce count 3+ multi-value entries into _accumulator dictionary
255+
foreach (var entry in _expandingAccumulator)
256+
{
257+
_accumulator[entry.Key] = new StringValues(entry.Value.ToArray());
258+
}
259+
}
260+
261+
return _accumulator ?? new AdaptiveCapacityDictionary<string, StringValues>(0, StringComparer.OrdinalIgnoreCase);
262+
}
263+
}
264+
265+
private static class SpanHelper
266+
{
267+
private static readonly SpanAction<char, IntPtr> s_replacePlusWithSpace = ReplacePlusWithSpaceCore;
268+
269+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
270+
public static unsafe string ReplacePlusWithSpace(ReadOnlySpan<char> span)
271+
{
272+
fixed (char* ptr = &MemoryMarshal.GetReference(span))
273+
{
274+
return string.Create(span.Length, (IntPtr)ptr, s_replacePlusWithSpace);
275+
}
276+
}
277+
278+
private static unsafe void ReplacePlusWithSpaceCore(Span<char> buffer, IntPtr state)
279+
{
280+
fixed (char* ptr = &MemoryMarshal.GetReference(buffer))
281+
{
282+
var input = (ushort*)state.ToPointer();
283+
var output = (ushort*)ptr;
284+
285+
var i = (nint)0;
286+
var n = (nint)(uint)buffer.Length;
287+
288+
if (Sse41.IsSupported && n >= Vector128<ushort>.Count)
289+
{
290+
var vecPlus = Vector128.Create((ushort)'+');
291+
var vecSpace = Vector128.Create((ushort)' ');
292+
293+
do
294+
{
295+
var vec = Sse2.LoadVector128(input + i);
296+
var mask = Sse2.CompareEqual(vec, vecPlus);
297+
var res = Sse41.BlendVariable(vec, vecSpace, mask);
298+
Sse2.Store(output + i, res);
299+
i += Vector128<ushort>.Count;
300+
} while (i <= n - Vector128<ushort>.Count);
301+
}
302+
303+
for (; i < n; ++i)
304+
{
305+
if (input[i] != '+')
306+
{
307+
output[i] = input[i];
308+
}
309+
else
310+
{
311+
output[i] = ' ';
312+
}
313+
}
314+
}
315+
}
316+
}
103317
}
104318
}

0 commit comments

Comments
 (0)