Skip to content

System.Text.Encodings.Web refactoring and code modernization - #49373

Merged
GrabYourPitchforks merged 13 commits into
dotnet:mainfrom
GrabYourPitchforks:encoder
Mar 19, 2021
Merged

System.Text.Encodings.Web refactoring and code modernization#49373
GrabYourPitchforks merged 13 commits into
dotnet:mainfrom
GrabYourPitchforks:encoder

Conversation

@GrabYourPitchforks

@GrabYourPitchforksGrabYourPitchforks commented Mar 9, 2021

Copy link
Copy Markdown
Member

Background:System.Text.Encodings.Web contains significant amounts of unsafe code. Part of this is due to the fact that the abstraction itself is pointer-based. And part of it is due to efforts to increase performance in hot paths. However, because these code patterns end up in hot paths and tight loops, it's difficult to foresee all the different edge cases that might crop up. This can manifest itself as a reliability or a security problem. Given that this code is intended to run over untrusted input, this is not ideal.

High-level overview of this PR

This PR refactors the System.Text.Encodings.Web project, modernizes much of the code to use Span<T> and other safer APIs as appropriate, and fixes a handful of outstanding bugs.

  • Unsafe code has been removed from hot paths where possible and refactored into separate reviewable and testable helper methods.
  • Vestigial code paths in both the source project and the test project have been removed. (The test project in particular contained many ancient artifacts and non-shipping adapters resulting from this code originally existing in the old pre-1.0 aspnet repository.)
  • The optimized workhorse logic for the inbox HTML, URL, JSON, and JSON-relaxed encoders have been moved into a single class, with the individual encoders now only responsible for dictating the representation of a single scalar value.
  • For custom encoders (where the user has subclassed one of our abstract types), we fall back to naïve but more universally correct logic.
  • For inbox encoders, the fast workhorse routine can make assumptions about how they'll escape data, giving significant performance wins over previous iterations of the logic. This optimized logic was previously unique to the JSON-relaxed encoder, but it has been generalized and extended to all inbox encoders as part of this refactoring.

A brief tour of the files

AllowedBmpCodePointsBitmap.cs - a bitmap of "allowed vs. disallowed" flags for all BMP characters. The implementation of this class is unsafe and requires close review. However, all entry points are guaranteed safe, and there's a standalone unit test exercising edge cases for the unsafe implementation.

AsciiByteMap.cs - a simple map of ASCII characters to single bytes, used for quick lookup by index. The implementation of this class is unsafe and requires close review. However, all entry points are guaranteed safe, and there's a standalone unit test exercising edge cases for the unsafe implementation.

Default[Html|Url|JavaScript]Encoder.cs - the in-box implementations of HtmlEncoder, UrlEncoder, and JavaScriptEncoder. There is no longer a separate implementation for the default inbox JSON encoder vs. the unsafe relaxed JSON encoder: they both filter down to shared logic in DefaultJavaScriptEncoder.cs. These files also contain the core "how do I perform HTML / URL / JS escaping?" logic. These files now contain only safe code, modulo overriding some existing unsafe APIs and forwarding the arguments elsewhere.

[Html|Url|JavaScript]Encoder.cs - provide static factories around the Default\*Encoder types. There's no longer any real logic in these types.

OptimizedInboxTextEncoder.cs - contains the shared "find which characters need escaping and write out the escaped form" logic used by all of the in-box encoders. There's no longer a separate code path for JSON vs. everything else. There are some unsafe method overrides, but for the most part they just forward arguments and don't do anything particularly interesting. The implementation of GetIndexOfFirstCharToEncode is unsafe and requires close review.

OptimizedInboxTextEncoder.Ascii.cs - contains optimized lookup tables for ASCII escaping. The implementation of these methods is unsafe and requires close review. However, all entry points are guaranteed safe, and there are standalone unit tests for these APIs.

OptimizedInboxTextEncoder.Ssse3.cs - contains SSE3-optimized "find the first char / byte to escape" logic. The implementation of these methods is unsafe and requires close review.

SpanUtility.cs - contains helper methods for working with and writing data to spans. The implementation of these methods is unsafe and requires close review. However, all entry points are guaranteed safe, and there are standalone unit tests for these APIs.

TextEncoder.cs - contains naïve "find which characters need escaping and write out the escaped form" logic that can work for generalized encoding that doesn't fulfill the contracts provided by our inbox encoders. There are also shared helper optimization methods for handling string escaping, etc. The implementation of these methods is safe, modulo some unsafe method overrides that forward to safe alternatives.

Polyfill\*.cs - contains internal polyfill implementations for APIs which are missing from downlevel.

Of special note is that the unsafe code is refactored in such a way that only the implementations bolded above have unsafe entry points. Other helper types which have unsafe implementations (like AsciiByteMap) have guaranteed-safe entry points and perform argument validation, and these helper types have their own suite of unit tests to help exercise edge cases. This should give high confidence that these helpers remain safe to call even in the face of a safely-written workhorse routine passing them bad data. The APIs bolded above (with unsafe entry points) are the ones that require closer review since they cannot be exercised in isolation from within unit tests. However, the unit test file InboxEncoderCommonTests.cs does try its best to provide various-length inputs to help detect issues. The unit tests are also scaffolded with the BoundedMemory<T> infrastructure to provide further detection of out-of-bounds memory accesses.

Performance

Performance numbers and discussion will be left as a comment within the issue.

Other notes for reviewers

The package no longer builds for netstandard2.1 or netcoreapp3.0. Instead, everything is unified as follows:

  • net60 - inbox version as part of the .NET 6 wave.
  • netcoreapp31 - OOB version to install into .NET Core 3.1 apps.
  • net461 - OOB version for .NET Framework 4.6.1+ (see Eric's comment here).
  • netstandard20 - OOB version for all other platforms and runtimes.

.NET Core 3.0 is already out of support, and .NET Core 2.1 will be out of support by the time this package RTMs. I don't think there's a need to include special DLLs targeting these runtimes. Additionally, even though this is not checked in yet, I'd like to stop harvesting the netstandard1.0 DLL into this package. Pretty much all apps should be targeting a netstandard2.0-capable platform at this point.

The existing SSE2 and ADVSIMD optimizations have been removed as part of this PR. The reason for this is that there's no longer a need for a "does this vector contain only ASCII bytes?" helper method. Instead, the SIMD ASCII-processing code paths have been written in terms of a pshufb-equivalent. For x86, this requires SSSE3.1. The ARM64 equivalent code path was never checked in to this library. That work will need to take place in order to restore the performance on ARM64. (/cc @carlossanlop@eiriktsarpalis)

Fixes#39829.
Fixes#45994.
Fixes#48519.

Ref: CVE-2021-26701 (MSRC 62749)

- Refactor unsafe code from TextEncoder workhorse routines into standalone helpers
- Fix bounds check logic in workhorse routines
- Remove vestigial code from the library and unit test project
- Add significant unit test coverage for the workhorse routines and unsafe helpers
@ghost

ghost commented Mar 9, 2021

Copy link
Copy Markdown

Tagging subscribers to this area: @tarekgh, @eiriktsarpalis, @layomia
See info in area-owners.md if you want to be subscribed.

Issue Details

Background:System.Text.Encodings.Web contains significant amounts of unsafe code. Part of this is due to the fact that the abstraction itself is pointer-based. And part of it is due to efforts to increase performance in hot paths. However, because these code patterns end up in hot paths and tight loops, it's difficult to foresee all the different edge cases that might crop up. This can manifest itself as a reliability or a security problem. Given that this code is intended to run over untrusted input, this is not ideal.

High-level overview of this PR

This PR refactors the System.Text.Encodings.Web project, modernizes much of the code to use Span<T> and other safer APIs as appropriate, and fixes a handful of outstanding bugs.

  • Unsafe code has been removed from hot paths where possible and refactored into separate reviewable and testable helper methods.
  • Vestigial code paths in both the source project and the test project have been removed. (The test project in particular contained many ancient artifacts and non-shipping adapters resulting from this code originally existing in the old pre-1.0 aspnet repository.)
  • The optimized workhorse logic for the inbox HTML, URL, JSON, and JSON-relaxed encoders have been moved into a single class, with the individual encoders now only responsible for dictating the representation of a single scalar value.
  • For custom encoders (where the user has subclassed one of our abstract types), we fall back to naïve but more universally correct logic.
  • For inbox encoders, the fast workhorse routine can make assumptions about how they'll escape data, giving significant performance wins over previous iterations of the logic. This optimized logic was previously unique to the JSON-relaxed encoder, but it has been generalized and extended to all inbox encoders as part of this refactoring.

A brief tour of the files

AllowedBmpCodePointsBitmap.cs - a bitmap of "allowed vs. disallowed" flags for all BMP characters. The implementation of this class is unsafe and requires close review. However, all entry points are guaranteed safe, and there's a standalone unit test exercising edge cases for the unsafe implementation.

AsciiByteMap.cs - a simple map of ASCII characters to single bytes, used for quick lookup by index. The implementation of this class is unsafe and requires close review. However, all entry points are guaranteed safe, and there's a standalone unit test exercising edge cases for the unsafe implementation.

Default[Html|Url|JavaScript]Encoder.cs - the in-box implementations of HtmlEncoder, UrlEncoder, and JavaScriptEncoder. There is no longer a separate implementation for the default inbox JSON encoder vs. the unsafe relaxed JSON encoder: they both filter down to shared logic in DefaultJavaScriptEncoder.cs. These files also contain the core "how do I perform HTML / URL / JS escaping?" logic. These files now contain only safe code, modulo overriding some existing unsafe APIs and forwarding the arguments elsewhere.

[Html|Url|JavaScript]Encoder.cs - provide static factories around the Default\*Encoder types. There's no longer any real logic in these types.

OptimizedInboxTextEncoder.cs - contains the shared "find which characters need escaping and write out the escaped form" logic used by all of the in-box encoders. There's no longer a separate code path for JSON vs. everything else. There are some unsafe method overrides, but for the most part they just forward arguments and don't do anything particularly interesting. The implementation of GetIndexOfFirstCharToEncode is unsafe and requires close review.

OptimizedInboxTextEncoder.Ascii.cs - contains optimized lookup tables for ASCII escaping. The implementation of these methods is unsafe and requires close review. However, all entry points are guaranteed safe, and there are standalone unit tests for these APIs.

OptimizedInboxTextEncoder.[Ssse3|Simd].cs - contains SSE3-optimized "find the first char / byte to escape" logic. The implementation of these methods is unsafe and requires close review.

SpanUtility.cs - contains helper methods for working with and writing data to spans. The implementation of these methods is unsafe and requires close review. However, all entry points are guaranteed safe, and there are standalone unit tests for these APIs.

TextEncoder.cs - contains naïve "find which characters need escaping and write out the escaped form" logic that can work for generalized encoding that doesn't fulfill the contracts provided by our inbox encoders. There are also shared helper optimization methods for handling string escaping, etc. The implementation of these methods is safe, modulo some unsafe method overrides that forward to safe alternatives.

Polyfill\*.cs - contains internal polyfill implementations for APIs which are missing from downlevel.

Of special note is that the unsafe code is refactored in such a way that only the implementations bolded above have unsafe entry points. Other helper types which have unsafe implementations (like AsciiByteMap) have guaranteed-safe entry points and perform argument validation, and these helper types have their own suite of unit tests to help exercise edge cases. This should give high confidence that these helpers remain safe to call even in the face of a safely-written workhorse routine passing them bad data. The APIs bolded above (with unsafe entry points) are the ones that require closer review since they cannot be exercised in isolation from within unit tests. However, the unit test file InboxEncoderCommonTests.cs does try its best to provide various-length inputs to help detect issues. The unit tests are also scaffolded with the BoundedMemory<T> infrastructure to provide further detection of out-of-bounds memory accesses.

Performance

Performance numbers and discussion will be left as a comment within the issue.

Other notes for reviewers

The package no longer builds for netstandard2.1, netcoreapp3.0, and net461. Instead, everything is unified as follows:

  • net60 - inbox version as part of the .NET 6 wave.
  • netcoreapp31 - OOB version to install into .NET Core 3.1 apps.
  • netstandard20 - OOB version for all other platforms and runtimes.

.NET Core 3.0 is already out of support, and .NET Core 2.1 will be out of support by the time this package RTMs. I don't think there's a need to include special DLLs targeting these runtimes. Additionally, even though this is not checked in yet, I'd like to stop harvesting the netstandard1.0 DLL into this package. Pretty much all apps should be targeting a netstandard2.0-capable platform at this point.

The existing SSE2 and ADVSIMD optimizations have been removed as part of this PR. The reason for this is that there's no longer a need for a "does this vector contain only ASCII bytes?" helper method. Instead, the SIMD ASCII-processing code paths have been written in terms of a pshufb-equivalent. For x86, this requires SSSE3.1. The ARM64 equivalent code path was never checked in to this library. That work will need to take place in order to restore the performance on ARM64. (/cc @carlossanlop@eiriktsarpalis)

Fixes #39829.
Fixes #45994.
Fixes #48519.

Ref: CVE-2021-26701 (MSRC 62749)

Author:GrabYourPitchforks
Assignees:-
Labels:

area-System.Text.Encodings.Web

Milestone:-

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Performance results

Raw performance numbers

MethodJobToolchainArgEncoderMeanErrorStdDevRatioRatioSD
FindFirstCharToEncodeUtf16Job-QJLEZHencoder<div (...)/div> [38]HTML8.694 ns0.0539 ns0.0478 ns2.920.03
FindFirstCharToEncodeUtf16Job-BRHPCWmain<div (...)/div> [38]HTML2.972 ns0.0230 ns0.0204 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoder<div (...)/div> [38]HTML6.325 ns0.0491 ns0.0435 ns0.020.00
FindFirstCharToEncodeUtf8Job-BRHPCWmain<div (...)/div> [38]HTML319.785 ns3.3888 ns3.0041 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoder<div (...)/div> [38]HTML91.049 ns1.2822 ns1.1366 ns0.430.01
EncodeToStringUtf16Job-BRHPCWmain<div (...)/div> [38]HTML211.922 ns1.3099 ns1.2253 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoder<div (...)/div> [38]HTML66.198 ns0.5649 ns0.5007 ns0.310.00
EncodeToBufferUtf16Job-BRHPCWmain<div (...)/div> [38]HTML216.714 ns0.9033 ns0.8008 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoder<div (...)/div> [38]HTML55.146 ns0.2212 ns0.1961 ns0.430.00
EncodeToBufferUtf8Job-BRHPCWmain<div (...)/div> [38]HTML128.954 ns0.6804 ns0.6031 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoder<div (...)/div> [38]JSON-Default7.189 ns0.1735 ns0.2065 ns1.380.04
FindFirstCharToEncodeUtf16Job-BRHPCWmain<div (...)/div> [38]JSON-Default5.258 ns0.0283 ns0.0265 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoder<div (...)/div> [38]JSON-Default5.606 ns0.0227 ns0.0212 ns0.960.01
FindFirstCharToEncodeUtf8Job-BRHPCWmain<div (...)/div> [38]JSON-Default5.867 ns0.0406 ns0.0339 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoder<div (...)/div> [38]JSON-Default102.222 ns0.6249 ns0.4879 ns0.440.00
EncodeToStringUtf16Job-BRHPCWmain<div (...)/div> [38]JSON-Default233.505 ns0.8457 ns0.7911 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoder<div (...)/div> [38]JSON-Default71.749 ns0.1845 ns0.1541 ns0.330.00
EncodeToBufferUtf16Job-BRHPCWmain<div (...)/div> [38]JSON-Default214.514 ns0.5218 ns0.4357 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoder<div (...)/div> [38]JSON-Default53.996 ns0.2096 ns0.1858 ns0.420.00
EncodeToBufferUtf8Job-BRHPCWmain<div (...)/div> [38]JSON-Default129.970 ns0.9891 ns0.8769 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoder<div (...)/div> [38]JSON-Relaxed7.055 ns0.0332 ns0.0311 ns0.850.01
FindFirstCharToEncodeUtf16Job-BRHPCWmain<div (...)/div> [38]JSON-Relaxed8.323 ns0.0383 ns0.0358 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoder<div (...)/div> [38]JSON-Relaxed5.615 ns0.0203 ns0.0190 ns0.950.01
FindFirstCharToEncodeUtf8Job-BRHPCWmain<div (...)/div> [38]JSON-Relaxed5.911 ns0.0436 ns0.0387 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoder<div (...)/div> [38]JSON-Relaxed59.882 ns0.4918 ns0.4600 ns0.390.00
EncodeToStringUtf16Job-BRHPCWmain<div (...)/div> [38]JSON-Relaxed154.046 ns1.1448 ns0.9560 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoder<div (...)/div> [38]JSON-Relaxed46.645 ns0.2107 ns0.1868 ns0.330.00
EncodeToBufferUtf16Job-BRHPCWmain<div (...)/div> [38]JSON-Relaxed139.924 ns0.3554 ns0.3151 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoder<div (...)/div> [38]JSON-Relaxed65.963 ns0.3243 ns0.3033 ns0.570.01
EncodeToBufferUtf8Job-BRHPCWmain<div (...)/div> [38]JSON-Relaxed115.284 ns0.9305 ns0.8704 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoder<div (...)/div> [38]URL7.178 ns0.0400 ns0.0355 ns1.880.03
FindFirstCharToEncodeUtf16Job-BRHPCWmain<div (...)/div> [38]URL3.817 ns0.0686 ns0.0642 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoder<div (...)/div> [38]URL6.263 ns0.0366 ns0.0343 ns0.020.00
FindFirstCharToEncodeUtf8Job-BRHPCWmain<div (...)/div> [38]URL326.791 ns4.4165 ns4.1312 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoder<div (...)/div> [38]URL84.182 ns1.0765 ns1.0069 ns0.380.00
EncodeToStringUtf16Job-BRHPCWmain<div (...)/div> [38]URL224.395 ns1.0459 ns0.9784 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoder<div (...)/div> [38]URL63.745 ns0.3234 ns0.3025 ns0.300.00
EncodeToBufferUtf16Job-BRHPCWmain<div (...)/div> [38]URL214.040 ns0.7012 ns0.5855 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoder<div (...)/div> [38]URL61.078 ns0.2520 ns0.2358 ns0.390.00
EncodeToBufferUtf8Job-BRHPCWmain<div (...)/div> [38]URL157.103 ns1.7207 ns1.6096 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoderThe q(...) dog. [44]HTML9.042 ns0.1458 ns0.1364 ns0.290.00
FindFirstCharToEncodeUtf16Job-BRHPCWmainThe q(...) dog. [44]HTML31.407 ns0.1234 ns0.1154 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoderThe q(...) dog. [44]HTML8.910 ns0.0453 ns0.0424 ns0.030.00
FindFirstCharToEncodeUtf8Job-BRHPCWmainThe q(...) dog. [44]HTML332.453 ns3.5721 ns3.3414 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoderThe q(...) dog. [44]HTML7.796 ns0.0469 ns0.0439 ns0.240.00
EncodeToStringUtf16Job-BRHPCWmainThe q(...) dog. [44]HTML32.159 ns0.1531 ns0.1432 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoderThe q(...) dog. [44]HTML15.169 ns0.0854 ns0.0798 ns0.370.00
EncodeToBufferUtf16Job-BRHPCWmainThe q(...) dog. [44]HTML41.074 ns0.3558 ns0.3328 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoderThe q(...) dog. [44]HTML13.366 ns0.0721 ns0.0674 ns0.120.00
EncodeToBufferUtf8Job-BRHPCWmainThe q(...) dog. [44]HTML114.260 ns0.7250 ns0.6782 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoderThe q(...) dog. [44]JSON-Default8.224 ns0.0471 ns0.0440 ns0.640.00
FindFirstCharToEncodeUtf16Job-BRHPCWmainThe q(...) dog. [44]JSON-Default12.845 ns0.0530 ns0.0496 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoderThe q(...) dog. [44]JSON-Default7.335 ns0.0394 ns0.0349 ns0.710.00
FindFirstCharToEncodeUtf8Job-BRHPCWmainThe q(...) dog. [44]JSON-Default10.261 ns0.0523 ns0.0437 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoderThe q(...) dog. [44]JSON-Default7.407 ns0.0617 ns0.0547 ns0.520.02
EncodeToStringUtf16Job-BRHPCWmainThe q(...) dog. [44]JSON-Default14.645 ns0.3429 ns0.4918 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoderThe q(...) dog. [44]JSON-Default15.193 ns0.0691 ns0.0612 ns0.730.00
EncodeToBufferUtf16Job-BRHPCWmainThe q(...) dog. [44]JSON-Default20.960 ns0.1974 ns0.1846 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoderThe q(...) dog. [44]JSON-Default14.895 ns0.3277 ns0.3218 ns0.120.00
EncodeToBufferUtf8Job-BRHPCWmainThe q(...) dog. [44]JSON-Default120.529 ns1.0115 ns0.8446 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoderThe q(...) dog. [44]JSON-Relaxed9.141 ns0.2108 ns0.2165 ns0.490.01
FindFirstCharToEncodeUtf16Job-BRHPCWmainThe q(...) dog. [44]JSON-Relaxed18.614 ns0.1031 ns0.0914 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoderThe q(...) dog. [44]JSON-Relaxed8.393 ns0.2006 ns0.5107 ns0.350.01
FindFirstCharToEncodeUtf8Job-BRHPCWmainThe q(...) dog. [44]JSON-Relaxed23.774 ns0.1035 ns0.0968 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoderThe q(...) dog. [44]JSON-Relaxed9.884 ns0.7963 ns2.3478 ns0.450.07
EncodeToStringUtf16Job-BRHPCWmainThe q(...) dog. [44]JSON-Relaxed21.797 ns0.1602 ns0.1499 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoderThe q(...) dog. [44]JSON-Relaxed15.261 ns0.0958 ns0.0849 ns0.590.00
EncodeToBufferUtf16Job-BRHPCWmainThe q(...) dog. [44]JSON-Relaxed25.686 ns0.1634 ns0.1448 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoderThe q(...) dog. [44]JSON-Relaxed14.727 ns0.1172 ns0.1039 ns0.130.00
EncodeToBufferUtf8Job-BRHPCWmainThe q(...) dog. [44]JSON-Relaxed114.112 ns0.7554 ns0.7066 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoderThe q(...) dog. [44]URL7.182 ns0.0645 ns0.0604 ns1.240.01
FindFirstCharToEncodeUtf16Job-BRHPCWmainThe q(...) dog. [44]URL5.801 ns0.0428 ns0.0357 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoderThe q(...) dog. [44]URL6.112 ns0.0390 ns0.0346 ns0.020.00
FindFirstCharToEncodeUtf8Job-BRHPCWmainThe q(...) dog. [44]URL327.942 ns1.8296 ns1.5278 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoderThe q(...) dog. [44]URL79.203 ns0.4831 ns0.4519 ns0.320.00
EncodeToStringUtf16Job-BRHPCWmainThe q(...) dog. [44]URL248.049 ns1.5316 ns1.4327 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoderThe q(...) dog. [44]URL58.186 ns0.2487 ns0.2205 ns0.250.00
EncodeToBufferUtf16Job-BRHPCWmainThe q(...) dog. [44]URL230.507 ns0.8315 ns0.7778 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoderThe q(...) dog. [44]URL57.857 ns0.2623 ns0.2325 ns0.330.00
EncodeToBufferUtf8Job-BRHPCWmainThe q(...) dog. [44]URL174.010 ns1.9451 ns1.8194 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoderЛорем(...) хис. [68]HTML8.513 ns0.0500 ns0.0468 ns2.440.01
FindFirstCharToEncodeUtf16Job-BRHPCWmainЛорем(...) хис. [68]HTML3.483 ns0.0177 ns0.0157 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoderЛорем(...) хис. [68]HTML11.363 ns0.0419 ns0.0392 ns0.040.00
FindFirstCharToEncodeUtf8Job-BRHPCWmainЛорем(...) хис. [68]HTML321.798 ns2.2619 ns2.1158 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoderЛорем(...) хис. [68]HTML656.485 ns5.2886 ns4.6882 ns0.720.01
EncodeToStringUtf16Job-BRHPCWmainЛорем(...) хис. [68]HTML914.875 ns6.3256 ns5.6075 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoderЛорем(...) хис. [68]HTML595.451 ns4.0190 ns3.5628 ns0.680.01
EncodeToBufferUtf16Job-BRHPCWmainЛорем(...) хис. [68]HTML878.506 ns17.5687 ns16.4337 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoderЛорем(...) хис. [68]HTML778.128 ns2.9941 ns2.6542 ns0.400.00
EncodeToBufferUtf8Job-BRHPCWmainЛорем(...) хис. [68]HTML1,940.292 ns21.4708 ns19.0333 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Default7.065 ns0.0454 ns0.0425 ns1.340.01
FindFirstCharToEncodeUtf16Job-BRHPCWmainЛорем(...) хис. [68]JSON-Default5.261 ns0.0289 ns0.0271 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Default10.291 ns0.1681 ns0.1573 ns1.760.03
FindFirstCharToEncodeUtf8Job-BRHPCWmainЛорем(...) хис. [68]JSON-Default5.842 ns0.0357 ns0.0298 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Default504.770 ns2.4158 ns2.2597 ns0.540.01
EncodeToStringUtf16Job-BRHPCWmainЛорем(...) хис. [68]JSON-Default933.988 ns18.2717 ns20.3089 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Default475.763 ns2.1843 ns1.9363 ns0.530.00
EncodeToBufferUtf16Job-BRHPCWmainЛорем(...) хис. [68]JSON-Default904.113 ns3.0337 ns2.3685 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Default571.503 ns3.2688 ns2.8977 ns0.280.00
EncodeToBufferUtf8Job-BRHPCWmainЛорем(...) хис. [68]JSON-Default2,044.989 ns18.8413 ns17.6242 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Relaxed38.437 ns0.2185 ns0.2043 ns0.580.01
FindFirstCharToEncodeUtf16Job-BRHPCWmainЛорем(...) хис. [68]JSON-Relaxed66.766 ns0.5176 ns0.4842 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Relaxed245.429 ns3.0871 ns2.8876 ns0.980.01
FindFirstCharToEncodeUtf8Job-BRHPCWmainЛорем(...) хис. [68]JSON-Relaxed251.336 ns1.4148 ns1.3234 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Relaxed37.532 ns0.1692 ns0.1582 ns0.540.00
EncodeToStringUtf16Job-BRHPCWmainЛорем(...) хис. [68]JSON-Relaxed70.017 ns0.2584 ns0.2158 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Relaxed43.956 ns0.1667 ns0.1559 ns0.600.00
EncodeToBufferUtf16Job-BRHPCWmainЛорем(...) хис. [68]JSON-Relaxed73.799 ns0.4131 ns0.3662 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoderЛорем(...) хис. [68]JSON-Relaxed250.019 ns1.6540 ns1.5472 ns0.640.01
EncodeToBufferUtf8Job-BRHPCWmainЛорем(...) хис. [68]JSON-Relaxed390.873 ns1.4870 ns1.3909 ns1.000.00
FindFirstCharToEncodeUtf16Job-QJLEZHencoderЛорем(...) хис. [68]URL7.149 ns0.0388 ns0.0363 ns1.810.01
FindFirstCharToEncodeUtf16Job-BRHPCWmainЛорем(...) хис. [68]URL3.954 ns0.0200 ns0.0187 ns1.000.00
FindFirstCharToEncodeUtf8Job-QJLEZHencoderЛорем(...) хис. [68]URL11.343 ns0.0374 ns0.0350 ns0.030.00
FindFirstCharToEncodeUtf8Job-BRHPCWmainЛорем(...) хис. [68]URL333.462 ns2.7372 ns2.5604 ns1.000.00
EncodeToStringUtf16Job-QJLEZHencoderЛорем(...) хис. [68]URL588.343 ns2.6690 ns2.2287 ns0.730.01
EncodeToStringUtf16Job-BRHPCWmainЛорем(...) хис. [68]URL800.937 ns6.3436 ns5.2972 ns1.000.00
EncodeToBufferUtf16Job-QJLEZHencoderЛорем(...) хис. [68]URL523.123 ns3.7607 ns3.5177 ns0.700.01
EncodeToBufferUtf16Job-BRHPCWmainЛорем(...) хис. [68]URL751.730 ns4.6596 ns4.3586 ns1.000.00
EncodeToBufferUtf8Job-QJLEZHencoderЛорем(...) хис. [68]URL607.154 ns2.3643 ns2.2115 ns0.320.00
EncodeToBufferUtf8Job-BRHPCWmainЛорем(...) хис. [68]URL1,889.955 ns7.3792 ns6.1619 ns1.000.00

Benchmark code

namespaceConsoleAppBenchmark{[SkipLocalsInit]publicclassTextEncoderRunner{[Params("The quick brown fox jumps over the lazy dog.",// no escaping needed ever"<div id=\"myDiv\">Escape &amp; me!</div>",// contains some HTML / URL / JSON-sensitive chars"Лорем ипсум долор сит амет, цоммуне малуиссет цонцлудатуряуе ад хис.")]// Cyrillic lipsum; no escaping needed (when Cyrillic allowed)publicstringArg{get;set;}privatebyte[]_argUtf8;privatechar[]_scratchBuffer=newchar[1024];privatebyte[]_scratchUtf8Buffer=newbyte[1024];[Params("HTML","URL","JSON-Default","JSON-Relaxed")]publicstringEncoder{get;set;}privateTextEncoder_encoder;[GlobalSetup]publicvoidSetup(){_argUtf8=Encoding.UTF8.GetBytes(Arg);_encoder=Encoderswitch{"HTML"=>HtmlEncoder.Default,"URL"=>UrlEncoder.Default,"JSON-Default"=>JavaScriptEncoder.Default,"JSON-Relaxed"=>JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
_ =>thrownewException("Unknown encoder."),};}[Benchmark]publicunsafeintFindFirstCharToEncodeUtf16(){stringarg=Arg;_=arg.Length;// deref; prove not null
fixed (char*pArg=arg){return_encoder.FindFirstCharacterToEncode(pArg,arg.Length);}}[Benchmark]publicintFindFirstCharToEncodeUtf8(){byte[]argUtf8=_argUtf8;_=argUtf8.Length;// deref; prove not nullreturn_encoder.FindFirstCharacterToEncodeUtf8(argUtf8);}[Benchmark]publicstringEncodeToStringUtf16(){return_encoder.Encode(Arg);}[Benchmark]publicOperationStatusEncodeToBufferUtf16(){stringarg=Arg;_=arg.Length;// deref; prove not nullchar[]dest=_scratchBuffer;_=dest.Length;// deref; prove not nullreturn_encoder.Encode(arg,dest,out_,out_);}[Benchmark]publicOperationStatusEncodeToBufferUtf8(){byte[]argUtf8=_argUtf8;_=argUtf8.Length;// deref; prove not nullbyte[]dest=_scratchUtf8Buffer;_=dest.Length;// deref; prove not nullreturn_encoder.EncodeUtf8(argUtf8,dest,out_,out_);}}}

Performance discussions

Performance is generally better across the board, often significantly so. The performance improvement comes from three main places:

  1. The "skip over all ASCII chars which don't require encoding" logic is now SIMD-optimized (on x64) for all encoders, not just the JSON encoder.
  2. The newly refactored helper methods reduce the number of unnecessary bounds checks in the safe workhorse routines. Bounds checking still takes place, but it is folded into the subsequent derefence or otherwise results in a future bounds check being elided where possible.
  3. The helper routines utilize data structures with simplified (C-style) memory layouts rather than bouncing through array-based indirections.

We also take advantage of recent PRs like #49180 to reduce the number of duplicate checks occurring inside our hot paths, opting to hoist these checks outside of the loop where possible.

The notable exception to the performance improvement is the FindFirstCharToEncodeUtf16 method. This method incurs a fixed (O(1)) overhead on method entry due to setting up the SIMD data structures. If the first character to encode occurs at the very beginning of the string, this overhead will show up as a 3 - 4 ns loss when compared to a simple char-by-char loop. Since the runtime of such a method was already very low, this 3 - 4 ns loss appears to be a significant overhead when seen as a ratio. I do not believe this to affect the common use case for these APIs, as the typical calling pattern is to call Encode(string) or similar API. That API uses FindFirstCharToEncode* as a workhorse routine, and the linear (O(n)) savings we see from the Encode* methods more than make up for any fixed loss due to SIMD overhead.

{
if (value.Value == '<')
{
if (!SpanUtility.TryWriteBytes(destination, (byte)'&', (byte)'l', (byte)'t', (byte)';')) { goto OutOfSpace; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something crucial here, but wouldn't it be better to pass an uint directly and do BinaryPrimitives.ReverseEndianness if required? ReverseEndianess is a bswap, compared to multiple shift and or in TryWriteBytes or even pass the appropriate reversed endianess version directly if required?
If I read the sharplab asm correcctly, apparently having something like

uintv=(byte)'&'<<24|(byte)'l'<<16|(byte)'t'<<8|(byte)';';if(BitConverter.IsLittleEndian){v=BinaryPrimitives.ReverseEndianness(v);}

is by now evaluated as a runtime constant (never realized that before)
https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKGIAYACY8gOgCEBXAMy5il3YBLAHbYoATwDcNek1YAlDsIyD8MFgGEI+AA6CANnwDKfAG6CwMXNNqNmLRctXqAksr4QdJqOcvWaMgDMTKQMGgwA3jQMMUzBxCgMALIAFACUkdGx2dlGOtjCADzA4hgwAHwMwPocwAwAvAy4GNhgANbY+voQYFWlMADapHQAujY5EwwcIhgMpg0MKSVlaQDkAGSrDIWFDKSJhIvLMGv6WzsM5EgMh0v9axjnuwAcN0f3q5KfWZOxglyLNiCDBaYSmPhlKAsFy4AAywIwhgAosIACaCAppH6/TLUHGTeaNIGiCQABSgqmBgnBAnkMHB/BgKPRBWEVlwKVMaXG+IYAF8AnjeQAVCQAdQpZTY/Q51VqaDm3OxAuo2OyAySMAwAAsIKiXLp9ClNTq9QadPoAPI6FQQYQCACCAHMnbBcLhqTA3PoRCInWkRmrYsR4lcqhAIPoGKLxBLgTBpWUOXkCsV+pVcPlhArpso5p0OCcgzEokL8f9FpmCixYTBhE6dQxyo0UFiyzjS7ycgBVe3YXgsONlXudQROtmowq5jDlFKwAGa/DQcRJMS4bWdFgAcS1dN4sGElhSVeEaQVpgLJx5XZixAA7AwMFBC9ecSquzB9LgYMWcp2b7eD5cJ036vr876TCqfJAA

that would allow you to keep the bytes visible and not require some "strange" magic value.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal was to keep the call site readable: "I'm writing these bytes in this order." If we want to change the implementation to call ReverseEndianness as an implementation detail I'm fine with that. You're right in that the ReverseEndianness API was intended to be optimized by the JIT as const input -> const output.

@gfoidlgfoidl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(only half-way throught the code so far...enough for me today)

uint value;
if (BitConverter.IsLittleEndian)
{
value = ((uint)d << 24) | ((uint)c << 16) | ((uint)b << 8) | a;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe writing the individual bytes is faster as

  • this produces quite a lot of machine code
  • cpus have a store buffer, so flushing to L1 will be done in bigger "chunks" anyway

A quick micro-benchmark (on kaby-lake) proves that:

| Method | Mean | Error | StdDev | Ratio | RatioSD |
|------- |---------:|----------:|----------:|------:|--------:|
| A | 1.335 ns | 0.0656 ns | 0.1022 ns | 1.00 | 0.00 |
| B | 1.178 ns | 0.0620 ns | 0.0608 ns | 0.85 | 0.08 |

A...your code
B...individiual writes

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the asm output on your box? On my box this code produces a single mov dword ptr [foo], CONST instruction, which beats the performance of four mov byte ptr [foo + i], CONST instructions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mov dword ptr [foo], CONST

Really a const if a, b, ... are arguments?
Or just in the specific case where the method is inlined and the arguments can be evaluated as constant values?
In this case of course that's better.

For asm (note: I'm not on latest main-branch):

; Bench.A(); ...cmpr8d,4jl short M00_L02shlecx,18shlr10d,10orecx,r10dshlr9d,8orecx,r9doreax,ecxmov[rdx],eaxmoveax,1jmp short M00_L03M00_L02:xoreax,eaxM00_L03:ret; Bench.B(); ...cmpr8d,4jl short M00_L02mov[rdx],almov[rdx+1],r9bmov[rdx+2],r10bmov[rdx+3],clmoveax,1jmp short M00_L03M00_L02:xoreax,eaxM00_L03:ret

The micro-benchmark is very flaky, but B is always faster.
Although it's a micro benchmark, that doesn't take into account that the store buffer may be full, only one store can be dispatched per cycle, etc. as it can be on real world workloads.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gfoidl The specific use case for this API is that all value parameters are constants. That's also called out in the devdoc on the API. This causes the JIT to const-fold everything.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I'm investigating why this is failing in CI. Unit tests pass cleanly on my box.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Hello JSON crew! You're pinged on this review because it changes the underlying System.Text.Encodings.Web implementation, and I had to adjust one of the System.Text.Json unit tests to account for the change. The System.Text.Json-specific change is cf8e998. It's a unit test only change. Basically, when the encoder sees invalid UTF-* data, it replaces that data with U+FFFD ('�') in the response. The unit test change makes the test resilient against the response containing either a literal '�' character or the escaped "\uFFFD" form, which are equivalent in JSON.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

The "all configurations" broken CI leg should be fixed by #49396.

- Update test csproj to include missing polyfills
- Fix net461 test compilation failures
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@gfoidlgfoidl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the SSE code and this looks good to me.

Left some nits.

} while ((i += 16) < lastLegalIterationFor16CharRead);
}

if ((lengthInBytes & 8) != 0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 this is clever and produces nice test jmp-combo that can be fused.

if (span.Length >= 6)
{
ulong value64;
uint value32;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: naming, we have value, hi, lo and these. Can this be unified? I like the value64 and value32 approach most.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed them to abcd and ef, depending on what values they're intended to hold. I think this naming is a little clearer. Let me know what you think!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know what you think!

Now it's clear and a good naming that I like.

(Sorry for not replying earlier)


if ((lengthInBytes & 3) != 0)
{
Debug.Assert(lengthInBytes - i <= 3);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the other branches have a Debug.Assert too?
Like Debug.Assert(lengthInBytes - i >= 8 && lengthInBytes - i < 16);, etc.

@eiriktsarpalis

Copy link
Copy Markdown
Member

Performance results

I'd be curious to see benchmark results on arm64.

@eerhardt

Copy link
Copy Markdown
Member

I'm seeing a ~4.3KB .br compressed size regression in the default Blazor WASM app with this change:

Left is before, right is after:

image

image

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I'd be curious to see benchmark results on arm64.

Might be a good excuse to learn how to compile and run perf tests on my SPX device. :)

Per the comments at the top of the issue, I expect this will regress performance on arm64 for the "nothing needs to be escaped" code paths. However, the arm64 code paths here really needed to be reworked anyway in order to support pshufb-like semantics. Once that work is done, I expect arm64 performance here to be better than it was for 5.0.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@eerhardt Interesting. I'm curious about the System.Private.CoreLib change in particular, as the only file touched there was https://github.com/dotnet/runtime/pull/49373/files#diff-3a22ee85ff262ccdbad92a3c073a523a039016bc6ee6cab752621493d6d46693, and I'm not sure how that trivial a change could have caused a 2KB regression. How does one begin investigating this?

@eerhardt

Copy link
Copy Markdown
Member

I'm not sure how that trivial a change could have caused a 2KB regression

With trimming, changes that are the most impactful are often the higher level changes that cause more, or less, code to be kept in dependent assemblies. So refactoring System.Text.Encodings.Web can change it to use more APIs / code from CoreLib. Which means those APIs can no longer be trimmed after the refactoring.

How does one begin investigating this?

Here are the steps I use to investigate size changes:

  1. Install the latest 6.0 SDK: https://github.com/dotnet/installer#installers-and-binaries
    • I usually install the .zip to some place like C:\dotnet and then put that on my $PATH
  2. dotnet new blazorwasm
  3. dotnet publish -c Release

This gets you the "before" app in bin\Release\net6.0\publish\wwwroot\_framework. You can see both the uncompressed .dll and the compressed .dll.br files. We care most about the .br compressed files' size. But you can use the uncompressed .dll for analysis.

Now to get that app to use your change, what I typically do is "replace the NuGet package files with locally built files". I'm sure there are other approaches, but I found this to be the easiest.

  1. Find the path to the runtime NuGet package being used
    • The way I do this is use /bl in the publish command above and look for the $task illink in the .binlog, and grab the linker command line, which shows the path.
    • It is of the form C:\Users\eerhardt\.nuget\packages\microsoft.netcore.app.runtime.browser-wasm\6.0.0-preview.3.21157.6\runtimes\browser-wasm\lib\net6.0
  2. Build the libraries you are changing locally for Release
    • .\build.cmd mono.corelib -os browser -arch wasm -c Release is how you build corelib for blazor wasm
  3. Copy the built libraries into the browser-wasm nuget package above, replacing the official libraries
  4. dotnet publish -c Release again, which will publish using your local libraries
  5. Compare / analyze the results

Note: Sometimes the latest SDK and the latest main branch have changes between them. So it is good practice to do steps 1-5 above using the "before your changes" commit and the "after your changes" commit. This is what I did to get the numbers above.

Tools for analysis I've found helpful are:

  • ILSpy to inspect what is and isn't there
  • ApiReviewer, and show internal methods, which will give you a diff of methods that are there now vs. before
  • Trimming Lens from the mono/linker repo

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@eerhardt With the latest commit (d13f660), I removed the Vector<T> dependency. This also should have removed the Vector128<T> dependency, because Vector128<T> now only exists as a field inside an explicit-layout fixed-size struct, and since nobody references that field it should be safe to remove both the field and any remaining compile-time references to the Vector128<T> type itself. But mono's iltrim utility for some reason is not taking this opportunity to trim that. If this is indeed a legal trim optimization (and I believe it is), then I think this is something that should be addressed in that tool rather than worked around on our side.

With this commit System.Private.CoreLib.dll.br is 1300 bytes above baseline.

I'm still looking into possible improvements in System.Text.Encodings.Web.dll itself.

@eerhardt

Copy link
Copy Markdown
Member

But mono's iltrim utility for some reason is not taking this opportunity to trim that. If this is indeed a legal trim optimization (and I believe it is), then I think this is something that should be addressed in that tool rather than worked around on our side.

Can you open an issue for this in https://github.com/mono/linker ?

@GrabYourPitchforks

GrabYourPitchforks commented Mar 11, 2021

Copy link
Copy Markdown
MemberAuthor

I can work around this for now by making AsVector a property instead of a field, but I'll need to disassemble again to make sure I'm not undoing the optimizations we got from #49180.

internalreadonlyref readonly Vector128<byte>AsVector=>refUnsafe.As<byte,Vector128<byte>>(ref Unsafe.AsRef(in AsBytes[0]));

Edit: Looks like it's interfering with the optimizations and causing register stack spilling. :(

@GrabYourPitchforks

GrabYourPitchforks commented Mar 17, 2021

Copy link
Copy Markdown
MemberAuthor

@eiriktsarpalis I was experimenting with arm64 SIMD enablement over in my personal fork based on some feedback from @tannergooding. I still need to test the code, but the logic in that file is fairly close to the logic in the SSSE3-specific code paths. Trying to figure out how to get it over to my SPX device for perf testing.

Edit: Something's going on with BenchmarkDotNet on that box, but I am able to perform some basic smoke testing. Things appear to be working correctly. I'll hold the ARM64 commit for now and send it as a separate PR once this is done. That way I can enlist help for running benchmarks and we can dedicate that separate PR just for ARM64-related discussion.

Edit x2: Basic console app and stopwatch never fails. :)

Baseline (release/5.0): 46 ns for HtmlEncoder.Default.FindFirstCharacterToEncodeUtf8(u8"The quick brown fox jumps over the lazy dog.")
advsimd-optimized: 8.8 ns for same input. (-81% wall clock time taken)

@GrabYourPitchforks

GrabYourPitchforks commented Mar 17, 2021

Copy link
Copy Markdown
MemberAuthor

Most recent 2 commits are unit test changes only to respond to PR feedback, no source or packaging changes.

@GrabYourPitchforks

GrabYourPitchforks commented Mar 17, 2021

Copy link
Copy Markdown
MemberAuthor

CI "build all configurations" failure is known issue which should be resolved by #49781.

Edit: Since that PR might bake for a few days, I've cherry-picked two updates to the package baseline in the latest iteration of this PR to help unblock CI. The resulting merge conflict should be minimal.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Latest commit (76f04f7) is merge from origin/main and conflict resolution, no code changes from previous PR review.

@GrabYourPitchforks

GrabYourPitchforks commented Mar 19, 2021

Copy link
Copy Markdown
MemberAuthor

Failing wasm test appears to be #48079.
Failing staging test appears to be a transient package server outage unrelated to the earlier "allConfigurations" packaging issues we were seeing.

@adamsitnik

Copy link
Copy Markdown
Member

@GrabYourPitchforks we got some nice improvements from this PR: DrewScoggins/performance-2#4632

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@adamsitnik This is probably also reflected in DrewScoggins/performance-2#4666. It's good to keep an eye on the System.Text.Json tests specifically to ensure that we didn't regress anything there.

@lewing

Copy link
Copy Markdown
Member

@GrabYourPitchforks we're seeing a big brower-wasm regression in System.Text.Json over a range that includes this #50260

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@lewing Thanks for the pointer. I'll respond over in that thread.

@ghostghost locked as resolved and limited conversation to collaborators Apr 25, 2021
@karelzkarelz added this to the 6.0.0 milestone May 20, 2021
@kunalspathak

kunalspathak commented Jun 8, 2022

Copy link
Copy Markdown
Contributor

I'll hold the ARM64 commit for now and send it as a separate PR once this is done

Was this ever done or did we ever measure the performance difference between x64 and arm64 and if there is a gap? Also, is there a tracking issue to optimize it for ARM64 (if it is slow)?

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Also, is there a tracking issue to optimize it for ARM64 (if it is slow)?

It was addressed by #49847.

@kunalspathak

Copy link
Copy Markdown
Contributor

Thanks. It is surprising that none of the MicroBenchmarks improvements were noticed or we might have missed triaging the improvements. CC: @DrewScoggins

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

13 participants

@GrabYourPitchforks@eiriktsarpalis@eerhardt@gfoidl@MichalStrehovsky@SingleAccretion@adamsitnik@lewing@kunalspathak@jkotas@Tornhoof@jeffhandley@karelz