Skip to content

Commit 92ff9a1

Browse files
gfoidlJamesNK
andauthored
Use perfect-hash in HttpUriScheme.GetKnownMethod(string) instead of trie (dotnet#44096)
Co-authored-by: James Newton-King <[email protected]>
1 parent de26f38 commit 92ff9a1

File tree

1 file changed

+104
-58
lines changed

1 file changed

+104
-58
lines changed

Diff for: src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs

+104-58
Original file line numberDiff line numberDiff line change
@@ -227,79 +227,125 @@ public static HttpMethod GetKnownMethod(this ReadOnlySpan<byte> span, out int me
227227
/// The Known Methods (CONNECT, DELETE, GET, HEAD, PATCH, POST, PUT, OPTIONS, TRACE)
228228
/// </remarks>
229229
/// <returns><see cref="HttpMethod"/></returns>
230-
public static HttpMethod GetKnownMethod(string value)
230+
public static HttpMethod GetKnownMethod(string? value)
231231
{
232-
// Called by http/2
233-
if (value == null)
232+
// A perfect hash is used to get an index into a lookup-table for know HTTP-methods.
233+
//
234+
// If value is not null or an empty string, then local function PerfectHash is called.
235+
// This perfect hashing is done by lookup up 'associatedValues' from a pre-generated lookup-table.
236+
// The generation of that table was done by GNU gperf tool.
237+
// Once we have that perfect hash we use another lookup-table to get the know HTTP-method if found
238+
// or return HttpMethod.Custom if not found.
239+
//
240+
// Further info and how to call gperf see https://github.com/dotnet/aspnetcore/pull/44096
241+
//
242+
// Code here could be removed if Roslyn improvements from
243+
// https://github.com/dotnet/roslyn/issues/56374 are added.
244+
245+
const int MinWordLength = 3;
246+
const int MaxWordLength = 7;
247+
const int MaxHashValue = 12;
248+
249+
if (string.IsNullOrEmpty(value))
234250
{
235251
return HttpMethod.None;
236252
}
237253

238-
var length = value.Length;
239-
if (length == 0)
254+
if (((uint)(value.Length - MinWordLength) <= (MaxWordLength - MinWordLength)))
240255
{
241-
return HttpMethod.None;
242-
}
256+
var methodsLookup = Methods();
243257

244-
// Start with custom and assign if known method is found
245-
var method = HttpMethod.Custom;
258+
Debug.Assert(WordListForPerfectHashOfMethods.Length == (MaxHashValue + 1) && methodsLookup.Length == (MaxHashValue + 1));
246259

247-
var firstChar = value[0];
248-
if (length == 3)
249-
{
250-
if (firstChar == 'G' && string.Equals(value, HttpMethods.Get, StringComparison.Ordinal))
251-
{
252-
method = HttpMethod.Get;
253-
}
254-
else if (firstChar == 'P' && string.Equals(value, HttpMethods.Put, StringComparison.Ordinal))
255-
{
256-
method = HttpMethod.Put;
257-
}
258-
}
259-
else if (length == 4)
260-
{
261-
if (firstChar == 'H' && string.Equals(value, HttpMethods.Head, StringComparison.Ordinal))
262-
{
263-
method = HttpMethod.Head;
264-
}
265-
else if (firstChar == 'P' && string.Equals(value, HttpMethods.Post, StringComparison.Ordinal))
266-
{
267-
method = HttpMethod.Post;
268-
}
269-
}
270-
else if (length == 5)
271-
{
272-
if (firstChar == 'T' && string.Equals(value, HttpMethods.Trace, StringComparison.Ordinal))
273-
{
274-
method = HttpMethod.Trace;
275-
}
276-
else if (firstChar == 'P' && string.Equals(value, HttpMethods.Patch, StringComparison.Ordinal))
277-
{
278-
method = HttpMethod.Patch;
279-
}
280-
}
281-
else if (length == 6)
282-
{
283-
if (firstChar == 'D' && string.Equals(value, HttpMethods.Delete, StringComparison.Ordinal))
260+
var index = PerfectHash(value);
261+
262+
if (index < (uint)WordListForPerfectHashOfMethods.Length
263+
&& WordListForPerfectHashOfMethods[index] == value
264+
&& index < (uint)methodsLookup.Length)
284265
{
285-
method = HttpMethod.Delete;
266+
return (HttpMethod)methodsLookup[(int)index];
286267
}
287268
}
288-
else if (length == 7)
269+
270+
return HttpMethod.Custom;
271+
272+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
273+
static uint PerfectHash(ReadOnlySpan<char> str)
289274
{
290-
if (firstChar == 'C' && string.Equals(value, HttpMethods.Connect, StringComparison.Ordinal))
275+
// This uses C#'s optimization to refer to static data of assembly, and doesn't allocate.
276+
ReadOnlySpan<byte> associatedValues = new byte[]
291277
{
292-
method = HttpMethod.Connect;
293-
}
294-
else if (firstChar == 'O' && string.Equals(value, HttpMethods.Options, StringComparison.Ordinal))
295-
{
296-
method = HttpMethod.Options;
297-
}
298-
}
299-
300-
return method;
278+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
279+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
280+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
281+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
282+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
283+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
284+
13, 13, 13, 13, 13, 13, 13, 5, 0, 13,
285+
13, 0, 0, 13, 13, 13, 13, 13, 13, 0,
286+
5, 13, 13, 13, 0, 13, 13, 13, 13, 13,
287+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
288+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
289+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
290+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
291+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
292+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
293+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
294+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
295+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
296+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
297+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
298+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
299+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
300+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
301+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
302+
13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
303+
13, 13, 13, 13, 13, 13
304+
};
305+
306+
var c = MemoryMarshal.GetReference(str);
307+
308+
Debug.Assert(char.IsAscii(c), "Must already be valiated");
309+
310+
return (uint)str.Length + associatedValues[c];
311+
}
312+
313+
// This uses C#'s optimization to refer to static data of assembly, and doesn't allocate.
314+
static ReadOnlySpan<byte> Methods() => new byte[]
315+
{
316+
(byte)HttpMethod.None,
317+
(byte)HttpMethod.None,
318+
(byte)HttpMethod.None,
319+
(byte)HttpMethod.Get,
320+
(byte)HttpMethod.Head,
321+
(byte)HttpMethod.Trace,
322+
(byte)HttpMethod.Delete,
323+
(byte)HttpMethod.Options,
324+
(byte)HttpMethod.Put,
325+
(byte)HttpMethod.Post,
326+
(byte)HttpMethod.Patch,
327+
(byte)HttpMethod.None,
328+
(byte)HttpMethod.Connect
329+
};
301330
}
302331

332+
private static readonly string[] WordListForPerfectHashOfMethods =
333+
{
334+
"",
335+
"",
336+
"",
337+
"GET",
338+
"HEAD",
339+
"TRACE",
340+
"DELETE",
341+
"OPTIONS",
342+
"PUT",
343+
"POST",
344+
"PATCH",
345+
"",
346+
"CONNECT"
347+
};
348+
303349
/// <summary>
304350
/// Checks 9 bytes from <paramref name="span"/> correspond to a known HTTP version.
305351
/// </summary>

0 commit comments

Comments
 (0)