Avoid substring allocations in WebUtility.HtmlDecode#29402
Avoid substring allocations in WebUtility.HtmlDecode#29402stephentoub merged 3 commits intodotnet:masterfrom
Conversation
| ["clubs"] = '\x2663', | ||
| ["hearts"] = '\x2665', | ||
| ["diams"] = '\x2666', | ||
| [1953461617] = '\x0022', // "quot" |
There was a problem hiding this comment.
This will be very hard to maintain. Since this table is computed once, I'd rather see a helper added that converts string to ulong, and then change each entry to just use that helper, e.g.
[ToUInt64Key("quot")] = '\x0022',You can then use the same helper in the lookup.
| // maps entity strings => unicode chars | ||
| private static readonly LowLevelDictionary<string, char> s_lookupTable = | ||
| new LowLevelDictionary<string, char>(Count, StringComparer.Ordinal) | ||
| private static readonly LowLevelDictionary<ulong, char> s_lookupTable = |
There was a problem hiding this comment.
Would it be faster if this is switched to regular Dictionary ?
LowLevelDictionary is left-over from the layering effort in the early .NET Core days. It is not as optimized as regular Dictionary.
There was a problem hiding this comment.
Outside of this change it seems we could aim to remove LLD from CoreFX entirely. It is only used here and in UriSyntax.cs.
There was a problem hiding this comment.
@jkotas worth me opening an issue to remove LLD from CoreFX? I am not sure how involved perf verification would have to be of such a change.
There was a problem hiding this comment.
I can confirm that replacing LowLevelDictionary with Dictionary does not affect the performance negatively. Here are some numbers (30000 calls to HtmlDecode per run):
LowLevelDictionary
| Run | Mean | Error | StdDev | Gen 0 | Allocated |
|---|---|---|---|---|---|
| 1 | 410.4 ms | 16.52 ms | 0.9337 ms | 59125.0000 | 177.61 MB |
| 2 | 412.0 ms | 32.60 ms | 1.842 ms | 59125.0000 | 177.61 MB |
| 3 | 411.4 ms | 36.08 ms | 2.039 ms | 59125.0000 | 177.61 MB |
| 4 | 410.2 ms | 1.824 ms | 0.1030 ms | 59125.0000 | 177.61 MB |
| 5 | 413.7 ms | 6.160 ms | 0.3480 ms | 59125.0000 | 177.61 MB |
Regular Dictionary
| Run | Mean | Error | StdDev | Gen 0 | Allocated |
|---|---|---|---|---|---|
| 1 | 413.5 ms | 18.43 ms | 1.041 ms | 56375.0000 | 169.37 MB |
| 2 | 406.3 ms | 18.16 ms | 1.026 ms | 56375.0000 | 169.37 MB |
| 3 | 406.9 ms | 45.48 ms | 2.570 ms | 56375.0000 | 169.37 MB |
| 4 | 410.8 ms | 84.31 ms | 4.763 ms | 56375.0000 | 169.37 MB |
| 5 | 411.4 ms | 35.25 ms | 1.992 ms | 56375.0000 | 169.37 MB |
| return default; | ||
| } | ||
|
|
||
| key |= (ulong)entity[i] << 8 * i; |
There was a problem hiding this comment.
Shift by variable amount tends to be more expensive than shift by fixed amount. It may be better to do it other way: key = (key << 8) | entity[i]
| else | ||
| { | ||
| string entity = value.Substring(entityOffset, entityLength); | ||
| ReadOnlySpan<char> entity = valueSpan.Slice(entityOffset, entityLength); |
There was a problem hiding this comment.
Rather than storing value.AsSpan() into a local and then using valueSpan.Slice(offset, count) in these three places, you can just use value.AsSpan(offset, count) in these three place.
- Use regular Dictionary instead of LowLevelDictionary - Use AsSpan overload instead of AsSpan() and Slice - Avoid shift by variable amount - Use helper method to generate keys to make the code easier to maintain
| { | ||
| if (entity[i] > 0xFF) | ||
| { | ||
| return default; |
|
|
||
| private static ulong ToUInt64Key(ReadOnlySpan<char> entity) | ||
| { | ||
| // The ulong key is the reversed single-byte character representation of the actual entity string. |
There was a problem hiding this comment.
It'd be good to assert here that the length is <= 8. If that ever doesn't hold, this in combination with the lookup table could give incorrect answers.
- Add assert to ToUInt64Key - Replace default with 0
|
Thanks! |
* Avoid substring allocations in WebUtility.HtmlDecode * Update changes to HtmlDecode based on feedback - Use regular Dictionary instead of LowLevelDictionary - Use AsSpan overload instead of AsSpan() and Slice - Avoid shift by variable amount - Use helper method to generate keys to make the code easier to maintain * Assert that entity length is <= 8 in ToUInt64Key - Add assert to ToUInt64Key - Replace default with 0
* Manually update the version of Microsoft.TargetingPack.Private.NETNative * Set Microsoft.TargetPack.Private.NETNative version to track live TFS version * Move Hashtable & friends to shared parition Include serialization info table for Hashtable Remove hashtable & hashprovider src link * Compile hashtable & friends only on uapaot * Fix build breaks * Remove Opcodes due to Opcodes moved to S.P.Corelib * Move Hashtable & friends to shared parition (dotnet/coreclr#17316) (#29307) * Move Hashtable & friends to shared parition * Move HashHelper serialization logic into its own file * Remove unchecked keyword in Hashtable Signed-off-by: dotnet-bot-corefx-mirror <dotnet-bot@microsoft.com> * Moving ConcurrentQueue to shared folder (dotnet/coreclr#17956) * Moving ConcurrentQueue to shared folder Fixes: #17751 Signed-off-by: dotnet-bot-corefx-mirror <dotnet-bot@microsoft.com> * Moving ConcurrentQueue to shared (dotnet/coreclr#18024) Making IProducerConsumerCollectionDebugView apis public Fixes: #17751 Signed-off-by: dotnet-bot-corefx-mirror <dotnet-bot@microsoft.com> * Modify corefx to use ConcurrentQueue from shared * Delete dead code after Hashtable.cs move (#29432) * Replace LowLevelDictionary with Dictionary (#29411) * Avoid substring allocations in WebUtility.HtmlDecode (#29402) * Avoid substring allocations in WebUtility.HtmlDecode * Update changes to HtmlDecode based on feedback - Use regular Dictionary instead of LowLevelDictionary - Use AsSpan overload instead of AsSpan() and Slice - Avoid shift by variable amount - Use helper method to generate keys to make the code easier to maintain * Assert that entity length is <= 8 in ToUInt64Key - Add assert to ToUInt64Key - Replace default with 0 * Remove CoreSetup, External, and Sni dependencies * Update the CoreFx dependency to release/2.1 so that Microsoft.NETCore.Platforms is defined correctly. Manually invoke the dependency update * Removing Fedora 26 and adding 28 as appropriate. Fix a couple README.md typos. * Remove Ubuntu 17.10 from CoreFX official runs
…9402) * Avoid substring allocations in WebUtility.HtmlDecode * Update changes to HtmlDecode based on feedback - Use regular Dictionary instead of LowLevelDictionary - Use AsSpan overload instead of AsSpan() and Slice - Avoid shift by variable amount - Use helper method to generate keys to make the code easier to maintain * Assert that entity length is <= 8 in ToUInt64Key - Add assert to ToUInt64Key - Replace default with 0 Commit migrated from dotnet/corefx@1681f77
Fixes #13960. In contrast to #27250, this PR focuses exclusively on the substring allocations and not on the StringBuilder/TextWriter part.
There are three substring allocations in WebUtility: L199, L203 and L234.
While the first two ones can be avoided by simply changing a few API calls since
uint.TryParsenow acceptsSpan<char>, the third one involves a bit more work as it requires the html entity lookup table to be modified. @jamesqo described a possible solution in #13960, however, as he pointed out, it might have an initialization impact. The one implemented here is less flexible, but comes without additional initialization costs. The idea is that because all supported html entity strings are 8 characters or less and are ASCII-only (and hence each character of the entity string can be represented by a single byte), they can be squeezed into an UInt64, which serves as the key in the lookup table. Instructions to compute the key for possible future entries are included in the code comments. A disadvantage of this approach is that potential future HTML entity strings of 9 characters or more would require an additional code path. That being said, HtmlDecode now runs up to 35% faster:I think that the performance improvements justify the added complexity and decreased readability.