Skip to content

Commit 21cc6e2

Browse files
authored
Minor performance improvements on hot code paths (#1947)
* Fix broken HTML tag in README.md * Remove HttpResponseMessage allocation in hot path * Remove iterator allocation from Select() in hot path * Replace allocating LINQ Reverse call with pre-sized reverse loop * Optimize reverse lookup in query string parsing * Use Convert.ToHexString (2x faster, 1/2 memory usage)
1 parent c0faae3 commit 21cc6e2

6 files changed

Lines changed: 39 additions & 44 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ We are grateful to the following sponsors, who provide the team with a no-cost l
303303
<a href="https://www.araxis.com">
304304
<img align="middle" src="docs/home/assets/img/araxis-logo.png" alt="Araxis Logo" style="width:150px">
305305
</a>
306-
<p/>
306+
</p>
307307

308308
Do you like this project? Consider to [sponsor](https://github.com/sponsors/json-api-dotnet), or just reward us by
309309
giving our repository a star.

src/JsonApiDotNetCore/Queries/Parsing/QueryExpressionParser.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,20 @@ protected virtual void Tokenize(string source)
5353
Source = source;
5454

5555
var tokenizer = new QueryTokenizer(source);
56-
TokenStack = new Stack<Token>(tokenizer.EnumerateTokens().Reverse());
56+
Token[] tokens = tokenizer.EnumerateTokens().ToArray();
57+
TokenStack = TokensToStack(tokens);
58+
}
59+
60+
private static Stack<Token> TokensToStack(Token[] tokens)
61+
{
62+
var stack = new Stack<Token>(tokens.Length);
63+
64+
for (int index = tokens.Length - 1; index >= 0; index--)
65+
{
66+
stack.Push(tokens[index]);
67+
}
68+
69+
return stack;
5770
}
5871

5972
/// <summary>
@@ -143,7 +156,11 @@ protected virtual void EatSingleCharacterToken(TokenKind kind)
143156
{
144157
if (!TokenStack.TryPop(out Token? token) || token.Kind != kind)
145158
{
146-
char ch = QueryTokenizer.SingleCharacterToTokenKinds.Single(pair => pair.Value == kind).Key;
159+
if (!QueryTokenizer.TokenKindToSingleCharacterLookup.TryGetValue(kind, out char ch))
160+
{
161+
throw new InvalidOperationException($"Token kind '{kind}' is not a single-character token.");
162+
}
163+
147164
int position = token?.Position ?? GetNextTokenPositionOrEnd();
148165
throw new QueryParseException($"{ch} expected.", position);
149166
}

src/JsonApiDotNetCore/Queries/Parsing/QueryTokenizer.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Collections.ObjectModel;
21
using System.Text;
32
using JetBrains.Annotations;
43

@@ -7,18 +6,20 @@ namespace JsonApiDotNetCore.Queries.Parsing;
76
[PublicAPI]
87
public sealed class QueryTokenizer
98
{
10-
public static readonly IReadOnlyDictionary<char, TokenKind> SingleCharacterToTokenKinds = new ReadOnlyDictionary<char, TokenKind>(
11-
new Dictionary<char, TokenKind>
12-
{
13-
['('] = TokenKind.OpenParen,
14-
[')'] = TokenKind.CloseParen,
15-
['['] = TokenKind.OpenBracket,
16-
[']'] = TokenKind.CloseBracket,
17-
['.'] = TokenKind.Period,
18-
[','] = TokenKind.Comma,
19-
[':'] = TokenKind.Colon,
20-
['-'] = TokenKind.Minus
21-
});
9+
public static readonly IReadOnlyDictionary<char, TokenKind> SingleCharacterToTokenKinds = new Dictionary<char, TokenKind>
10+
{
11+
['('] = TokenKind.OpenParen,
12+
[')'] = TokenKind.CloseParen,
13+
['['] = TokenKind.OpenBracket,
14+
[']'] = TokenKind.CloseBracket,
15+
['.'] = TokenKind.Period,
16+
[','] = TokenKind.Comma,
17+
[':'] = TokenKind.Colon,
18+
['-'] = TokenKind.Minus
19+
}.AsReadOnly();
20+
21+
internal static readonly IReadOnlyDictionary<TokenKind, char> TokenKindToSingleCharacterLookup =
22+
SingleCharacterToTokenKinds.ToDictionary(pair => pair.Value, pair => pair.Key).AsReadOnly();
2223

2324
private readonly string _source;
2425
private readonly StringBuilder _textBuffer = new();

src/JsonApiDotNetCore/Serialization/Response/FingerprintGenerator.cs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@ namespace JsonApiDotNetCore.Serialization.Response;
77
internal sealed class FingerprintGenerator : IFingerprintGenerator
88
{
99
private static readonly byte[] Separator = "|"u8.ToArray();
10-
private static readonly uint[] LookupTable = Enumerable.Range(0, 256).Select(ToLookupEntry).ToArray();
11-
12-
private static uint ToLookupEntry(int index)
13-
{
14-
string hex = index.ToString("X2");
15-
return hex[0] + ((uint)hex[1] << 16);
16-
}
1710

1811
/// <inheritdoc />
1912
public string Generate(IEnumerable<string> elements)
@@ -30,22 +23,6 @@ public string Generate(IEnumerable<string> elements)
3023
}
3124

3225
byte[] hash = hasher.GetHashAndReset();
33-
return ByteArrayToHex(hash);
34-
}
35-
36-
private static string ByteArrayToHex(byte[] bytes)
37-
{
38-
// https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa
39-
40-
char[] buffer = new char[bytes.Length * 2];
41-
42-
for (int index = 0; index < bytes.Length; index++)
43-
{
44-
uint value = LookupTable[bytes[index]];
45-
buffer[2 * index] = (char)value;
46-
buffer[2 * index + 1] = (char)(value >> 16);
47-
}
48-
49-
return new string(buffer);
26+
return Convert.ToHexString(hash);
5027
}
5128
}

src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ private static bool CanWriteBody(HttpStatusCode statusCode)
118118

119119
private static bool IsSuccessStatusCode(HttpStatusCode statusCode)
120120
{
121-
return new HttpResponseMessage(statusCode).IsSuccessStatusCode;
121+
return (int)statusCode is >= 200 and <= 299;
122122
}
123123

124124
private string RenderModel(object? model)

src/JsonApiDotNetCore/Serialization/Response/ResourceObjectTreeNode.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,11 @@ public IList<ResourceObject> GetResponseIncluded()
184184
HashSet<ResourceObject> primaryResourceObjectSet = GetDirectChildren().Select(node => node.ResourceObject).ToHashSet(ResourceObjectComparer.Instance);
185185
List<ResourceObject> includes = [];
186186

187-
foreach (ResourceObject include in visited.Select(node => node.ResourceObject))
187+
foreach (ResourceObjectTreeNode include in visited)
188188
{
189-
if (!primaryResourceObjectSet.Contains(include))
189+
if (!primaryResourceObjectSet.Contains(include.ResourceObject))
190190
{
191-
includes.Add(include);
191+
includes.Add(include.ResourceObject);
192192
}
193193
}
194194

0 commit comments

Comments
 (0)