From 9ce69c9da5a6477754c42b727d5c53e74f5d6c14 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Wed, 19 Aug 2026 12:13:56 -0400 Subject: [PATCH 1/2] Add AVX-512 Ice Lake decoder and retarget the library to .NET 10. Port simdutf's Ice Lake base64 kernel (VPERMI2B lookup, VPCOMPRESSB whitespace compression, masked 48-byte stores) using the .NET 10 AVX-512 VBMI/VBMI2 intrinsics. Dispatch prefers AVX-512 VBMI2, then AVX2, SSSE3, or scalar. Benchmarks on a Xeon Gold 6548N reach 11.3 GB/s versus 4.7 GB/s for System.Buffers.Text.Base64.DecodeFromUtf8. --- .github/workflows/docs.yml | 4 +- .github/workflows/dotnet.yml | 5 +- README.md | 15 +- benchmark/Benchmark.cs | 23 + benchmark/benchmark.csproj | 2 +- docs/articles/benchmarks.md | 4 +- docs/articles/contributing.md | 2 + docs/articles/getting-started.md | 9 +- docs/articles/how-it-works.md | 18 +- docs/docfx.json | 2 +- docs/index.md | 19 +- src/Base64.cs | 17 +- src/Base64AVX512UTF16.cs | 691 +++++++++++++++++++++++++++ src/Base64AVX512UTF8.cs | 787 +++++++++++++++++++++++++++++++ src/SimdBase64.csproj | 2 +- test/Base64DecodingTestsUTF16.cs | 154 ++++++ test/Base64DecodingTestsUTF8.cs | 133 ++++++ test/TestHelpers.cs | 2 +- test/tests.csproj | 2 +- 19 files changed, 1845 insertions(+), 46 deletions(-) create mode 100644 src/Base64AVX512UTF16.cs create mode 100644 src/Base64AVX512UTF8.cs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4f2e411..b4e5124 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,10 +27,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup .NET 9.0 + - name: Setup .NET 10.0 uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Install DocFX run: dotnet tool update -g docfx diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 7a799e3..b9e8ad4 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,11 +13,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET 9.0 (preview) + - name: Setup .NET 10.0 uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x - dotnet-quality: preview + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/README.md b/README.md index bc507b0..1b755fa 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ fully reproducible. |:----------------|:------------------------|:-------------------|:-------------------| | Apple M2 processor (ARM, 3.5 Ghz) | 10 | 3.8 | 2.6 x | | AWS Graviton 3 (ARM, 2.6 GHz) | 5.1 | 2.0 | 2.6 x | -| Intel Ice Lake (2.0 GHz) | 7.6 | 3.4 | 2.2 x | +| Intel Xeon Gold 6548N (AVX-512, 2.8 GHz) | 11.3 | 4.7 | 2.4 x | +| Intel Ice Lake (AVX2, 2.0 GHz) | 7.6 | 3.4 | 2.2 x | | AMD EPYC 7R32 (Zen 2, 2.8 GHz) | 6.9 | 3.0 | 2.3 x | ## Results (SimdBase64 vs. string .NET functions) @@ -59,17 +60,20 @@ byte[] newBytes = SimdBase64.Base64.FromBase64String(s); | processor and base freq. | SimdBase64 (GB/s) | .NET speed (GB/s) | speed up | |:----------------|:------------------------|:-------------------|:-------------------| | Apple M2 processor (ARM, 3.5 Ghz) | 4.0 | 1.1 | 3.6 x | +| Intel Xeon Gold 6548N (AVX-512, 2.8 GHz) | 2.1 | 0.71 | 2.9 x | | Intel Ice Lake (2.0 GHz) | 2.5 | 0.65 | 3.8 x | ## AVX-512 -As for .NET 9, the support for AVX-512 remains incomplete in C#. In particular, important -VBMI2 instructions are missing. Hence, we are not using AVX-512 under x64 systems at this time. -However, as soon as .NET offers the necessary support, we will update our results. +On .NET 10, we use AVX-512 VBMI / VBMI2 when the CPU supports them (Ice Lake and later, +including the Xeon Gold 6548N numbers above). The kernel is a C# port of the +[simdutf Ice Lake decoder](https://github.com/simdutf/simdutf): a 64-byte +`VPERMI2B` lookup, `VPCOMPRESSB` to strip white space, and a masked 48-byte store. +On older x64 CPUs the library still dispatches to AVX2 or SSSE3. ## Requirements -We require .NET 9 or better: https://dotnet.microsoft.com/en-us/download/dotnet/9.0 +We require .NET 10 or better: https://dotnet.microsoft.com/en-us/download/dotnet/10.0 ## Usage @@ -177,6 +181,7 @@ You can convert an integer to a hex string like so: `$"0x{MyVariable:X}"`. ## Performance tips - Be careful: `Vector128.Shuffle` is not the same as `Ssse3.Shuffle` nor is `Vector256.Shuffle` the same as `Avx2.Shuffle`. Prefer the latter. +- Likewise `Vector512.Shuffle` is a full 64-byte permute; `Avx512BW.Shuffle` is lane-wise `VPSHUFB`. For the Ice Lake kernel use `Avx512Vbmi.PermuteVar64x8` / `PermuteVar64x8x2`. - Similarly `Vector128.Shuffle` is not the same as `AdvSimd.Arm64.VectorTableLookup`, use the latter. - `stackalloc` arrays should probably not be used in class instances. - In C#, `struct` might be preferable to `class` instances as it makes it clear that the data is thread local. diff --git a/benchmark/Benchmark.cs b/benchmark/Benchmark.cs index 873412e..b26a5cd 100644 --- a/benchmark/Benchmark.cs +++ b/benchmark/Benchmark.cs @@ -321,6 +321,24 @@ public unsafe void RunSSEDecodingBenchmarkWithAllocUTF16(string[] data, int[] le } } + public unsafe void RunAVX512DecodingBenchmarkUTF8(string[] data, int[] lengths) + { + for (int i = 0; i < FileContent.Length; i++) + { + byte[] base64 = input[i]; + byte[] dataoutput = output[i]; + int bytesConsumed = 0; + int bytesWritten = 0; + SimdBase64.AVX512.Base64.DecodeFromBase64AVX512(base64.AsSpan(), dataoutput, out bytesConsumed, out bytesWritten, false); + if (bytesWritten != lengths[i]) + { + Console.WriteLine($"Error: {bytesWritten} != {lengths[i]}"); +#pragma warning disable CA2201 + throw new Exception("Error"); + } + } + } + public unsafe void RunAVX2DecodingBenchmarkUTF8(string[] data, int[] lengths) { for (int i = 0; i < FileContent.Length; i++) @@ -620,6 +638,11 @@ public unsafe void AVX2DecodingRealDataUTF8() RunAVX2DecodingBenchmarkUTF8(FileContent, DecodedLengths); } + public unsafe void AVX512DecodingRealDataUTF8() + { + RunAVX512DecodingBenchmarkUTF8(FileContent, DecodedLengths); + } + [Benchmark] [BenchmarkCategory("default")] public unsafe void SimdBase64DecodingRealDataUTF8() diff --git a/benchmark/benchmark.csproj b/benchmark/benchmark.csproj index f49dcaf..364387a 100644 --- a/benchmark/benchmark.csproj +++ b/benchmark/benchmark.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable true diff --git a/docs/articles/benchmarks.md b/docs/articles/benchmarks.md index 628ba7f..0e08784 100644 --- a/docs/articles/benchmarks.md +++ b/docs/articles/benchmarks.md @@ -29,7 +29,8 @@ decoder. SimdBase64 is **1.7×–2.6×** faster on realistic inputs of a few kil |:------------------------------------|:-----------------:|:-----------:|:--------:| | Apple M2 (ARM, 3.5 GHz) | 10 | 3.8 | 2.6× | | AWS Graviton 3 (ARM, 2.6 GHz) | 5.1 | 2.0 | 2.6× | -| Intel Ice Lake (2.0 GHz) | 7.6 | 3.4 | 2.2× | +| Intel Xeon Gold 6548N (AVX-512, 2.8 GHz) | 11.3 | 4.7 | 2.4× | +| Intel Ice Lake (AVX2, 2.0 GHz) | 7.6 | 3.4 | 2.2× | | AMD EPYC 7R32 (Zen 2, 2.8 GHz) | 6.9 | 3.0 | 2.3× | ## vs. `Convert.FromBase64String` @@ -40,6 +41,7 @@ The .NET runtime does **not** accelerate `Convert.FromBase64String`. Replacing i | processor and base freq. | SimdBase64 (GB/s) | .NET (GB/s) | speed-up | |:------------------------------------|:-----------------:|:-----------:|:--------:| | Apple M2 (ARM, 3.5 GHz) | 4.0 | 1.1 | 3.6× | +| Intel Xeon Gold 6548N (AVX-512, 2.8 GHz) | 2.1 | 0.71 | 2.9× | | Intel Ice Lake (2.0 GHz) | 2.5 | 0.65 | 3.8× | > Hardware, runtime version and input all affect these numbers. Treat the tables as diff --git a/docs/articles/contributing.md b/docs/articles/contributing.md index 23dee15..e6604bd 100644 --- a/docs/articles/contributing.md +++ b/docs/articles/contributing.md @@ -91,6 +91,8 @@ A few hard-won tips when working on the SIMD kernels: - `Vector128.Shuffle` is **not** the same as `Ssse3.Shuffle`, nor is `Vector256.Shuffle` the same as `Avx2.Shuffle`. Prefer the architecture-specific intrinsics. +- `Vector512.Shuffle` is a full 64-byte permute; `Avx512BW.Shuffle` is lane-wise `VPSHUFB`. + The Ice Lake kernel uses `Avx512Vbmi.PermuteVar64x8` / `PermuteVar64x8x2`. - Likewise, `Vector128.Shuffle` differs from `AdvSimd.Arm64.VectorTableLookup`; use the latter on ARM. - Avoid `stackalloc` arrays in class instances. - Prefer `struct` over `class` to make thread-local data explicit. diff --git a/docs/articles/getting-started.md b/docs/articles/getting-started.md index fdd87f6..a9a3b1b 100644 --- a/docs/articles/getting-started.md +++ b/docs/articles/getting-started.md @@ -1,11 +1,11 @@ # Getting started SimdBase64 is a small, dependency-free C# library that decodes base64 with SIMD -instructions. It targets **.NET 9** (or better) and runs on x64 and ARM64. +instructions. It targets **.NET 10** (or better) and runs on x64 and ARM64. ## Requirements -- [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) or newer. +- [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) or newer. - A 64-bit x64 or ARM64 CPU for the SIMD kernels (a portable scalar fallback covers everything else). ## Build & reference @@ -94,7 +94,8 @@ byte[] bytes = SimdBase64.Base64.FromBase64String(s); ## Choosing a specific kernel `DecodeFromBase64` dispatches to the fastest kernel your CPU supports. The architecture-specific -implementations live in nested namespaces (`SimdBase64.Arm`, `SimdBase64.AVX2`, `SimdBase64.SSE`, -`SimdBase64.Scalar`) and can be called directly — useful for testing or pinning behaviour. +implementations live in nested namespaces (`SimdBase64.Arm`, `SimdBase64.AVX512`, +`SimdBase64.AVX2`, `SimdBase64.SSE`, `SimdBase64.Scalar`) and can be called directly — useful +for testing or pinning behaviour. Continue to [How it works](how-it-works.md) or jump to the [API reference](xref:SimdBase64.Base64). diff --git a/docs/articles/how-it-works.md b/docs/articles/how-it-works.md index c81f155..d178a7f 100644 --- a/docs/articles/how-it-works.md +++ b/docs/articles/how-it-works.md @@ -35,25 +35,27 @@ At a high level, each vectorized block: A single public method picks the best kernel for the host CPU, in priority order: ```text -ARM64 NEON → AVX2 → SSE4.2 / SSSE3 → scalar fallback +ARM64 NEON → AVX-512 VBMI2 → AVX2 → SSSE3 → scalar fallback ``` -This means you write one call and automatically get NEON on an Apple M-series laptop, AVX2 on a -current x64 server, and a correct scalar implementation everywhere else. +This means you write one call and automatically get NEON on an Apple M-series laptop, AVX-512 +on Ice Lake and later x64 servers, AVX2 on older x64, and a correct scalar implementation +everywhere else. | Back-end | Vector width | Typical hardware | |----------|--------------|------------------| +| AVX-512 VBMI2 | 512-bit | Ice Lake, Sapphire Rapids, Emerald Rapids, Zen 4+ | | AVX2 | 256-bit | Most current x64 | | SSE4.2 / SSSE3 | 128-bit | Older x64 | | ARM64 NEON | 128-bit | Apple Silicon, AWS Graviton, Snapdragon | | Scalar | — | Portable fallback | -## What about AVX-512? +## AVX-512 -As of .NET 9, the C# support for AVX-512 is still incomplete — in particular the VBMI2 -instructions this algorithm relies on are missing. So SimdBase64 does **not** use AVX-512 under -x64 at this time. As soon as the runtime exposes the necessary intrinsics, we will add a kernel -and update the benchmarks. +On .NET 10 the VBMI / VBMI2 intrinsics (`PermuteVar64x8x2`, `Compress`) are available, so we +ship an Ice Lake kernel ported from [simdutf](https://github.com/simdutf/simdutf). It processes +64 input bytes per iteration, compresses white space with `VPCOMPRESSB`, and writes exactly 48 +decoded bytes with a masked store. ## Why an `OperationStatus`, not a `bool`? diff --git a/docs/docfx.json b/docs/docfx.json index c39a9ca..70ac4f8 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -13,7 +13,7 @@ "memberLayout": "separatePages", "enumSortOrder": "declaringOrder", "properties": { - "TargetFramework": "net9.0" + "TargetFramework": "net10.0" } } ], diff --git a/docs/index.md b/docs/index.md index 33271bd..35fe6c7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ title: SimdBase64 — fast base64 decoding for .NET

SimdBase64

-

A blazing-fast C# library for WHATWG forgiving-base64 decoding — up to 2.3× faster than the accelerated .NET functions and 3.8× faster than Convert.FromBase64String, using AVX2, SSE and ARM NEON.

+

A blazing-fast C# library for WHATWG forgiving-base64 decoding — up to 2.6× faster than the accelerated .NET functions and 3.8× faster than Convert.FromBase64String, using AVX-512, AVX2, SSE and ARM NEON.

Get started → API reference @@ -25,8 +25,8 @@ title: SimdBase64 — fast base64 decoding for .NET
faster than Convert.FromBase64String
-
3
-
SIMD back-ends: AVX2, SSE4.2, NEON
+
4
+
SIMD back-ends: AVX-512, AVX2, SSE4.2, NEON
0
@@ -62,7 +62,7 @@ Already calling `Convert.FromBase64String`? Swap in the accelerated version with byte[] bytes = SimdBase64.Base64.FromBase64String(s); ``` -The right SIMD kernel is selected automatically at runtime: **ARM64 NEON**, **AVX2**, **SSE4.2 / SSSE3**, or a portable scalar fallback. +The right SIMD kernel is selected automatically at runtime: **ARM64 NEON**, **AVX-512**, **AVX2**, **SSE4.2 / SSSE3**, or a portable scalar fallback.
@@ -73,7 +73,7 @@ The right SIMD kernel is selected automatically at runtime: **ARM64 NEON**, **AV
🧭

Runtime dispatch

-

One call, the best available kernel. AVX2, SSE4.2, ARM NEON or a scalar fallback — chosen for your CPU.

+

One call, the best available kernel. AVX-512, AVX2, SSE4.2, ARM NEON or a scalar fallback — chosen for your CPU.

🧹
@@ -92,10 +92,11 @@ The right SIMD kernel is selected automatically at runtime: **ARM64 NEON**, **AV Decoding throughput against the accelerated .NET functions (`System.Buffers.Text.Base64.DecodeFromUtf8`) on the enron email corpus. Longer bars are faster — SimdBase64 in purple, the .NET standard library in grey.
-
Apple M2 (NEON)
10 GB/s
3.8
2.6×
-
Intel Ice Lake
7.6 GB/s
3.4
2.2×
-
AMD EPYC (Zen 2)
6.9 GB/s
3.0
2.3×
-
AWS Graviton 3
5.1 GB/s
2.0
2.6×
+
Xeon Gold 6548N (AVX-512)
11.3 GB/s
4.7
2.4×
+
Apple M2 (NEON)
10 GB/s
3.8
2.6×
+
Intel Ice Lake (AVX2)
7.6 GB/s
3.4
2.2×
+
AMD EPYC (Zen 2)
6.9 GB/s
3.0
2.3×
+
AWS Graviton 3
5.1 GB/s
2.0
2.6×

Against the unaccelerated Convert.FromBase64String, the gap is even larger — 3.6×–3.8×. See the full set of measurements in the benchmarks.

diff --git a/src/Base64.cs b/src/Base64.cs index 15ca40d..1757f51 100644 --- a/src/Base64.cs +++ b/src/Base64.cs @@ -32,10 +32,10 @@ public unsafe static OperationStatus DecodeFromBase64(ReadOnlySpan source, { return Arm.Base64.DecodeFromBase64ARM(source, dest, out bytesConsumed, out bytesWritten, isUrl); } - // To be completed, this may have to wait for .NET 10. - //if (Vector512.IsHardwareAccelerated && Avx512Vbmi2.IsSupported) - //{ - //} + if (Avx512Vbmi2.IsSupported && Popcnt.X64.IsSupported) + { + return AVX512.Base64.DecodeFromBase64AVX512(source, dest, out bytesConsumed, out bytesWritten, isUrl); + } if (Avx2.IsSupported && Popcnt.IsSupported && Bmi1.IsSupported) { return AVX2.Base64.DecodeFromBase64AVX2(source, dest, out bytesConsumed, out bytesWritten, isUrl); @@ -56,11 +56,10 @@ public unsafe static OperationStatus DecodeFromBase64(ReadOnlySpan source, { return Arm.Base64.DecodeFromBase64ARM(source, dest, out bytesConsumed, out bytesWritten, isUrl); } - // To be completed, this may have to wait for .NET 10. - //if (Vector512.IsHardwareAccelerated && Avx512Vbmi.IsSupported) - //{ - // return GetPointerToFirstInvalidByteAvx512(pInputBuffer, inputLength, out Utf16CodeUnitCountAdjustment, out ScalarCodeUnitCountAdjustment); - //} + if (Avx512Vbmi2.IsSupported && Popcnt.X64.IsSupported) + { + return AVX512.Base64.DecodeFromBase64AVX512(source, dest, out bytesConsumed, out bytesWritten, isUrl); + } if (Avx2.IsSupported && Popcnt.IsSupported && Bmi1.IsSupported) { return AVX2.Base64.DecodeFromBase64AVX2(source, dest, out bytesConsumed, out bytesWritten, isUrl); diff --git a/src/Base64AVX512UTF16.cs b/src/Base64AVX512UTF16.cs new file mode 100644 index 0000000..f5f7abb --- /dev/null +++ b/src/Base64AVX512UTF16.cs @@ -0,0 +1,691 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; + +namespace SimdBase64 +{ + namespace AVX512 + { + public static partial class Base64 + { + // Caller is responsible for checking that Avx512Vbmi2.IsSupported && Popcnt.X64.IsSupported + public unsafe static OperationStatus DecodeFromBase64AVX512(ReadOnlySpan source, Span dest, out int bytesConsumed, out int bytesWritten, bool isUrl = false) + { + + if (isUrl) + { + return InnerDecodeFromBase64AVX512Url(source, dest, out bytesConsumed, out bytesWritten); + } + else + { + return InnerDecodeFromBase64AVX512Regular(source, dest, out bytesConsumed, out bytesWritten); + } + } + + private unsafe static OperationStatus InnerDecodeFromBase64AVX512Regular(ReadOnlySpan source, Span dest, out int bytesConsumed, out int bytesWritten) + { + // translation from ASCII to 6 bit values + bool isUrl = false; + bytesConsumed = 0; + bytesWritten = 0; + const int blocksSize = 6; + // Should be + // Span buffer = stackalloc byte[blocksSize * 64]; + Span buffer = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + // Define pointers within the fixed blocks + fixed (char* srcInit = source) + fixed (byte* dstInit = dest) + fixed (byte* startOfBuffer = buffer) + { + char* srcEnd = srcInit + source.Length; + char* src = srcInit; + byte* dst = dstInit; + byte* dstEnd = dstInit + dest.Length; + + int whiteSpaces = 0; + int equalsigns = 0; + + int bytesToProcess = source.Length; + // skip trailing spaces + while (bytesToProcess > 0 && SimdBase64.Scalar.Base64.IsAsciiWhiteSpace((char)source[bytesToProcess - 1])) + { + bytesToProcess--; + whiteSpaces++; + } + + int equallocation = bytesToProcess; // location of the first padding character if any + if (bytesToProcess > 0 && source[bytesToProcess - 1] == '=') + { + bytesToProcess -= 1; + equalsigns++; + while (bytesToProcess > 0 && SimdBase64.Scalar.Base64.IsAsciiWhiteSpace((char)source[bytesToProcess - 1])) + { + bytesToProcess--; + whiteSpaces++; + } + if (bytesToProcess > 0 && source[bytesToProcess - 1] == '=') + { + equalsigns++; + bytesToProcess -= 1; + } + } + + + { + byte* bufferPtr = startOfBuffer; + + ulong bufferBytesConsumed = 0;//Only used if there is an error + ulong bufferBytesWritten = 0;//Only used if there is an error + + if (bytesToProcess >= 64) + { + char* srcEnd64 = srcInit + bytesToProcess - 64; + while (src <= srcEnd64) + { + + Base64.Block64 b; + Base64.LoadBlock(&b, src); + src += 64; + bufferBytesConsumed += 64; + bool error = false; + UInt64 badCharMask = Base64.ToBase64Mask(isUrl, &b, ref error); + if (error == true) + { + src -= bufferBytesConsumed; + dst -= bufferBytesWritten; + + bytesConsumed = Math.Max(0, (int)(src - srcInit)); + bytesWritten = Math.Max(0, (int)(dst - dstInit)); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + if (badCharMask != 0) + { + // optimization opportunity: check for simple masks like those made of + // continuous 1s followed by continuous 0s. And masks containing a + // single bad character. + ulong compressedBytesCount = CompressBlock(ref b, badCharMask, bufferPtr); + bufferPtr += compressedBytesCount; + bufferBytesConsumed += compressedBytesCount; + + } + else if (bufferPtr != startOfBuffer) + { + + CopyBlock(&b, bufferPtr); + bufferPtr += 64; + bufferBytesConsumed += 64; + } + else + { + Base64DecodeBlock(dst, &b); + bufferBytesWritten += 48; + dst += 48; + } + + if (bufferPtr >= (blocksSize - 1) * 64 + startOfBuffer) // We treat the last block separately later on + { + for (int i = 0; i < (blocksSize - 2); i++) // We also treat the second to last block differently! Until then it is safe to proceed: + { + Base64DecodeBlock(dst, startOfBuffer + i * 64); + bufferBytesWritten += 48; + dst += 48; + } + Base64DecodeBlock(dst, startOfBuffer + (blocksSize - 2) * 64); + + dst += 48; + Buffer.MemoryCopy(startOfBuffer + (blocksSize - 1) * 64, startOfBuffer, 64, 64); + bufferPtr -= (blocksSize - 1) * 64; + + bufferBytesWritten = 0; + bufferBytesConsumed = 0; + } + + } + } + // Optimization note: if this is almost full, then it is worth our + // time, otherwise, we should just decode directly. + + + int lastBlock = (int)((bufferPtr - startOfBuffer) % 64); + int lastBlockSrcCount = 0; + // There is at some bytes remaining beyond the last 64 bit block remaining + if (lastBlock != 0 && srcEnd - src + lastBlock >= 64) // We first check if there is any error and eliminate white spaces?: + { + while ((bufferPtr - startOfBuffer) % 64 != 0 && src < srcEnd) + { + if (!SimdBase64.Scalar.Base64.IsValidBase64Index(*src)) + { + bytesConsumed = Math.Max(0, (int)(src - srcInit) - lastBlockSrcCount - (int)bufferBytesConsumed); + bytesWritten = Math.Max(0, (int)(dst - dstInit) - (int)bufferBytesWritten); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + byte val = SimdBase64.Tables.GetToBase64Value((uint)*src); + *bufferPtr = val; + if (val > 64) + { + bytesConsumed = Math.Max(0, (int)(src - srcInit) - lastBlockSrcCount - (int)bufferBytesConsumed); + bytesWritten = Math.Max(0, (int)(dst - dstInit) - (int)bufferBytesWritten); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + bufferPtr += (val <= 63) ? 1 : 0; + src++; + lastBlockSrcCount++; + } + + } + + byte* subBufferPtr = startOfBuffer; + for (; subBufferPtr + 64 <= bufferPtr; subBufferPtr += 64) + { + + Base64DecodeBlock(dst, subBufferPtr); + dst += 48;// 64 bits of base64 decodes to 48 bits + } + if ((bufferPtr - subBufferPtr) % 64 != 0) + { + while (subBufferPtr + 4 < bufferPtr) // we decode one base64 element (4 bit) at a time + { + + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 4, 4); + + dst += 3; + subBufferPtr += 4; + } + if (subBufferPtr + 4 <= bufferPtr) // this may be the very last element, might be incomplete + { + + + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 3, 3); + + dst += 3; + subBufferPtr += 4; + } + int leftover = (int)(bufferPtr - subBufferPtr); + if (leftover > 0) + { + + while (leftover < 4 && src < srcEnd) + { + if (!SimdBase64.Scalar.Base64.IsValidBase64Index(*src)) + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.InvalidData; + } + byte val = SimdBase64.Tables.GetToBase64Value((byte)*src); + if (val > 64) + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.InvalidData; + } + subBufferPtr[leftover] = (byte)(val); + leftover += (val <= 63) ? 1 : 0; + src++; + } + + if (leftover == 1) + { + + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.NeedMoreData; + } + if (leftover == 2) + { + UInt32 triple = ((UInt32)(subBufferPtr[0]) << 3 * 6) + + ((UInt32)(subBufferPtr[1]) << 2 * 6); + triple = BinaryPrimitives.ReverseEndianness(triple); + triple >>= 8; + Buffer.MemoryCopy(&triple, dst, 1, 1); + + dst += 1; + } + else if (leftover == 3) + { + UInt32 triple = ((UInt32)(subBufferPtr[0]) << 3 * 6) + + ((UInt32)(subBufferPtr[1]) << 2 * 6) + + ((UInt32)(subBufferPtr[2]) << 1 * 6); + triple = BinaryPrimitives.ReverseEndianness(triple); + + triple >>= 8; + + Buffer.MemoryCopy(&triple, dst, 2, 2); + dst += 2; + } + else + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 3, 3); + dst += 3; + } + } + } + + + if (src < srcEnd + equalsigns) // We finished processing 64-bit blocks, we're not quite at the end yet + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.DecodeFromBase64Scalar(source.Slice(bytesConsumed, bytesToProcess - bytesConsumed), dest.Slice(bytesWritten), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + if (result == OperationStatus.InvalidData) + { + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + else + { + bytesConsumed += remainderBytesConsumed + (source.Length - bytesToProcess); + bytesWritten += remainderBytesWritten; + } + if (result == OperationStatus.Done && equalsigns > 0) + { + // additional checks + if ((remainderBytesWritten % 3 == 0) || ((remainderBytesWritten % 3) + 1 + equalsigns != 4)) + { + result = OperationStatus.InvalidData; + } + } + return result; + } + if (equalsigns > 0) // final additional check + { + if (((int)(dst - dstInit) % 3 == 0) || (((int)(dst - dstInit) % 3) + 1 + equalsigns != 4)) + { + return OperationStatus.InvalidData; + } + } + + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.Done; + } + + } + } + + private unsafe static OperationStatus InnerDecodeFromBase64AVX512Url(ReadOnlySpan source, Span dest, out int bytesConsumed, out int bytesWritten) + { + // translation from ASCII to 6 bit values + bool isUrl = true; + bytesConsumed = 0; + bytesWritten = 0; + const int blocksSize = 6; + // Should be + // Span buffer = stackalloc byte[blocksSize * 64]; + Span buffer = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + // Define pointers within the fixed blocks + fixed (char* srcInit = source) + fixed (byte* dstInit = dest) + fixed (byte* startOfBuffer = buffer) + { + char* srcEnd = srcInit + source.Length; + char* src = srcInit; + byte* dst = dstInit; + byte* dstEnd = dstInit + dest.Length; + + int whiteSpaces = 0; + int equalsigns = 0; + + int bytesToProcess = source.Length; + // skip trailing spaces + while (bytesToProcess > 0 && SimdBase64.Scalar.Base64.IsAsciiWhiteSpace((char)source[bytesToProcess - 1])) + { + bytesToProcess--; + whiteSpaces++; + } + + int equallocation = bytesToProcess; // location of the first padding character if any + if (bytesToProcess > 0 && source[bytesToProcess - 1] == '=') + { + bytesToProcess -= 1; + equalsigns++; + while (bytesToProcess > 0 && SimdBase64.Scalar.Base64.IsAsciiWhiteSpace((char)source[bytesToProcess - 1])) + { + bytesToProcess--; + whiteSpaces++; + } + if (bytesToProcess > 0 && source[bytesToProcess - 1] == '=') + { + equalsigns++; + bytesToProcess -= 1; + } + } + + + { + byte* bufferPtr = startOfBuffer; + + ulong bufferBytesConsumed = 0;//Only used if there is an error + ulong bufferBytesWritten = 0;//Only used if there is an error + + if (bytesToProcess >= 64) + { + char* srcEnd64 = srcInit + bytesToProcess - 64; + while (src <= srcEnd64) + { + Base64.Block64 b; + Base64.LoadBlock(&b, src); + src += 64; + bufferBytesConsumed += 64; + bool error = false; + UInt64 badCharMask = Base64.ToBase64Mask(isUrl, &b, ref error); + if (error == true) + { + src -= bufferBytesConsumed; + dst -= bufferBytesWritten; + + bytesConsumed = Math.Max(0, (int)(src - srcInit)); + bytesWritten = Math.Max(0, (int)(dst - dstInit)); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + if (badCharMask != 0) + { + // optimization opportunity: check for simple masks like those made of + // continuous 1s followed by continuous 0s. And masks containing a + // single bad character. + ulong compressedBytesCount = CompressBlock(ref b, badCharMask, bufferPtr); + bufferPtr += compressedBytesCount; + bufferBytesConsumed += compressedBytesCount; + + + } + else if (bufferPtr != startOfBuffer) + { + CopyBlock(&b, bufferPtr); + bufferPtr += 64; + bufferBytesConsumed += 64; + } + else + { + Base64DecodeBlock(dst, &b); + bufferBytesWritten += 48; + dst += 48; + } + + if (bufferPtr >= (blocksSize - 1) * 64 + startOfBuffer) // We treat the last block separately later on + { + for (int i = 0; i < (blocksSize - 2); i++) // We also treat the second to last block differently! Until then it is safe to proceed: + { + Base64DecodeBlock(dst, startOfBuffer + i * 64); + bufferBytesWritten += 48; + dst += 48; + } + Base64DecodeBlock(dst, startOfBuffer + (blocksSize - 2) * 64); + + + + dst += 48; + Buffer.MemoryCopy(startOfBuffer + (blocksSize - 1) * 64, startOfBuffer, 64, 64); + bufferPtr -= (blocksSize - 1) * 64; + + bufferBytesWritten = 0; + bufferBytesConsumed = 0; + } + + } + } + // Optimization note: if this is almost full, then it is worth our + // time, otherwise, we should just decode directly. + int lastBlock = (int)((bufferPtr - startOfBuffer) % 64); + // There is at some bytes remaining beyond the last 64 bit block remaining + if (lastBlock != 0 && srcEnd - src + lastBlock >= 64) // We first check if there is any error and eliminate white spaces?: + { + int lastBlockSrcCount = 0; + while ((bufferPtr - startOfBuffer) % 64 != 0 && src < srcEnd) + { + + if (!SimdBase64.Scalar.Base64.IsValidBase64Index(*src)) + { + bytesConsumed = Math.Max(0, (int)(src - srcInit) - lastBlockSrcCount - (int)bufferBytesConsumed); + bytesWritten = Math.Max(0, (int)(dst - dstInit) - (int)bufferBytesWritten); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + byte val = Tables.GetToBase64UrlValue((byte)*src); + *bufferPtr = val; + if (val > 64) + { + bytesConsumed = Math.Max(0, (int)(src - srcInit) - lastBlockSrcCount - (int)bufferBytesConsumed); + bytesWritten = Math.Max(0, (int)(dst - dstInit) - (int)bufferBytesWritten); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + bufferPtr += (val <= 63) ? 1 : 0; + src++; + lastBlockSrcCount++; + } + } + + byte* subBufferPtr = startOfBuffer; + for (; subBufferPtr + 64 <= bufferPtr; subBufferPtr += 64) + { + Base64DecodeBlock(dst, subBufferPtr); + + dst += 48;// 64 bits of base64 decodes to 48 bits + } + if ((bufferPtr - subBufferPtr) % 64 != 0) + { + while (subBufferPtr + 4 < bufferPtr) // we decode one base64 element (4 bit) at a time + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 4, 4); + + dst += 3; + subBufferPtr += 4; + } + if (subBufferPtr + 4 <= bufferPtr) // this may be the very last element, might be incomplete + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 3, 3); + + dst += 3; + subBufferPtr += 4; + } + int leftover = (int)(bufferPtr - subBufferPtr); + if (leftover > 0) + { + + if (!SimdBase64.Scalar.Base64.IsValidBase64Index(*src)) + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.InvalidData; + } + + + while (leftover < 4 && src < srcEnd) + { + byte val = Tables.GetToBase64UrlValue((byte)*src); + if (val > 64) + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.InvalidData; + } + subBufferPtr[leftover] = (byte)(val); + leftover += (val <= 63) ? 1 : 0; + src++; + } + + if (leftover == 1) + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.NeedMoreData; + } + if (leftover == 2) + { + UInt32 triple = ((UInt32)(subBufferPtr[0]) << 3 * 6) + + ((UInt32)(subBufferPtr[1]) << 2 * 6); + triple = BinaryPrimitives.ReverseEndianness(triple); + triple >>= 8; + Buffer.MemoryCopy(&triple, dst, 1, 1); + + dst += 1; + } + else if (leftover == 3) + { + UInt32 triple = ((UInt32)(subBufferPtr[0]) << 3 * 6) + + ((UInt32)(subBufferPtr[1]) << 2 * 6) + + ((UInt32)(subBufferPtr[2]) << 1 * 6); + triple = BinaryPrimitives.ReverseEndianness(triple); + + triple >>= 8; + + Buffer.MemoryCopy(&triple, dst, 2, 2); + + dst += 2; + } + else + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 3, 3); + + dst += 3; + } + } + } + + if (src < srcEnd + equalsigns) // We finished processing 64-bit blocks, we're not quite at the end yet + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.DecodeFromBase64Scalar(source.Slice(bytesConsumed, bytesToProcess - bytesConsumed), dest.Slice(bytesWritten), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + if (result == OperationStatus.InvalidData) + { + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + else + { + bytesConsumed += remainderBytesConsumed + (source.Length - bytesToProcess); + bytesWritten += remainderBytesWritten; + } + if (result == OperationStatus.Done && equalsigns > 0) + { + + // additional checks + if ((remainderBytesWritten % 3 == 0) || ((remainderBytesWritten % 3) + 1 + equalsigns != 4)) + { + result = OperationStatus.InvalidData; + } + } + return result; + } + if (equalsigns > 0) // final additional check + { + if (((int)(dst - dstInit) % 3 == 0) || (((int)(dst - dstInit) % 3) + 1 + equalsigns != 4)) + { + return OperationStatus.InvalidData; + } + } + + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.Done; + } + + } + } + } + } +} diff --git a/src/Base64AVX512UTF8.cs b/src/Base64AVX512UTF8.cs new file mode 100644 index 0000000..319b268 --- /dev/null +++ b/src/Base64AVX512UTF8.cs @@ -0,0 +1,787 @@ +using System; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Buffers; +using System.Buffers.Binary; + +namespace SimdBase64 +{ + namespace AVX512 + { + public static partial class Base64 + { + // Ice Lake (AVX-512 VBMI / VBMI2) kernels, ported from simdutf: + // https://github.com/simdutf/simdutf/blob/master/src/icelake/icelake_base64.inl.cpp + // Tables from _mm512_set_epi8 are reversed for Vector512.Create (lowest lane first). + + [StructLayout(LayoutKind.Sequential)] + private struct Block64 + { + public Vector512 chunk0; + } + + // First 48 bytes of a decoded 64-character block. + private static readonly Vector512 DecodeStoreMask = Vector512.Create( + Vector256.Create(byte.MaxValue), + Vector256.Create( + byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue, + byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue, + byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue, + byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue, + (byte)0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void LoadBlock(Block64* b, byte* src) + { + b->chunk0 = Avx512BW.LoadVector512(src); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void LoadBlock(Block64* b, char* src) + { + Vector512 m1 = Avx512BW.LoadVector512((short*)src); + Vector512 m2 = Avx512BW.LoadVector512((short*)(src + 32)); + Vector512 packed = Avx512BW.PackUnsignedSaturate(m1, m2); + Vector512 laneOrder = Vector512.Create(0L, 2L, 4L, 6L, 1L, 3L, 5L, 7L); + b->chunk0 = Avx512F.PermuteVar8x64(packed.AsInt64(), laneOrder).AsByte(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ulong ToBase64Mask(bool base64Url, Block64* b, ref bool error) + { + Vector512 input = b->chunk0; + + Vector512 asciiSpaceTbl = Vector512.Create( + (byte)32, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 0, 12, 13, 0, 0, + 32, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 0, 12, 13, 0, 0, + 32, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 0, 12, 13, 0, 0, + 32, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 0, 12, 13, 0, 0); + + Vector512 lookup0; + Vector512 lookup1; + if (base64Url) + { + lookup0 = Vector512.Create( + (sbyte)-1, -128, -128, -128, -128, -128, -128, -128, -128, -1, -1, -128, -128, -1, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -1, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, 62, -128, -128, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -128, -128, -128, -128, -128, -128).AsByte(); + lookup1 = Vector512.Create( + (sbyte)-128, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -128, -128, -128, -128, 63, + -128, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -128, -128, -128, -128, -128).AsByte(); + } + else + { + lookup0 = Vector512.Create( + (sbyte)-128, -128, -128, -128, -128, -128, -128, -128, -128, -1, -1, -128, -128, -1, -128, -128, + -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, + -1, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, 62, -128, -128, -128, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -128, -128, -128, -128, -128, -128).AsByte(); + lookup1 = Vector512.Create( + (sbyte)-128, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -128, -128, -128, -128, -128, + -128, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -128, -128, -128, -128, -128).AsByte(); + } + + Vector512 translated = Avx512Vbmi.PermuteVar64x8x2(lookup0, input, lookup1); + Vector512 combined = Avx512F.Or(translated.AsInt64(), input.AsInt64()).AsByte(); + ulong mask = combined.ExtractMostSignificantBits(); + if (mask != 0) + { + Vector512 shuffled = Avx512BW.Shuffle(asciiSpaceTbl, input); + ulong spaces = Avx512BW.CompareEqual(shuffled, input).ExtractMostSignificantBits(); + error |= (mask != spaces); + } + + b->chunk0 = translated; + return mask; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ulong CompressBlock(ref Block64 b, ulong mask, byte* output) + { + ulong nmask = ~mask; + Vector512 keepMask = MaskFromUInt64(nmask); + Vector512 compressed = Avx512Vbmi2.Compress(Vector512.Zero, keepMask, b.chunk0); + Avx512BW.Store(output, compressed); + return Popcnt.X64.PopCount(nmask); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 MaskFromUInt64(ulong mask) + { + Vector256 repeat = Vector256.Create( + (byte)0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3); + Vector256 bits = Vector256.Create( + (byte)1, 2, 4, 8, 16, 32, 64, 128, + 1, 2, 4, 8, 16, 32, 64, 128, + 1, 2, 4, 8, 16, 32, 64, 128, + 1, 2, 4, 8, 16, 32, 64, 128); + Vector256 loSrc = Vector256.Create(mask).AsByte(); + Vector256 hiSrc = Vector256.Create(mask >> 32).AsByte(); + Vector256 lo = Avx2.CompareEqual(Avx2.And(Avx2.Shuffle(loSrc, repeat), bits), bits); + Vector256 hi = Avx2.CompareEqual(Avx2.And(Avx2.Shuffle(hiSrc, repeat), bits), bits); + return Vector512.Create(lo, hi); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void CopyBlock(Block64* b, byte* output) + { + Avx512BW.Store(output, b->chunk0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void Base64Decode(byte* output, Vector512 input) + { + Vector512 mergeAbAndBc = Avx512BW.MultiplyAddAdjacent(input, Vector512.Create(unchecked((int)0x01400140)).AsSByte()); + Vector512 merged = Avx512BW.MultiplyAddAdjacent(mergeAbAndBc, Vector512.Create(0x00011000).AsInt16()); + Vector512 pack = Vector512.Create( + (byte)2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, 18, 17, 16, 22, + 21, 20, 26, 25, 24, 30, 29, 28, 34, 33, 32, 38, 37, 36, 42, 41, + 40, 46, 45, 44, 50, 49, 48, 54, 53, 52, 58, 57, 56, 62, 61, 60, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + Vector512 shuffled = Avx512Vbmi.PermuteVar64x8(merged.AsByte(), pack); + Avx512BW.MaskStore(output, DecodeStoreMask, shuffled); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void Base64DecodeBlock(byte* outPtr, byte* srcPtr) + { + Base64Decode(outPtr, Avx512BW.LoadVector512(srcPtr)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void Base64DecodeBlock(byte* output, Block64* block) + { + Base64Decode(output, block->chunk0); + } + + // Caller is responsible for checking that Avx512Vbmi2.IsSupported && Popcnt.X64.IsSupported + public unsafe static OperationStatus DecodeFromBase64AVX512(ReadOnlySpan source, Span dest, out int bytesConsumed, out int bytesWritten, bool isUrl = false) + { + if (isUrl) + { + return InnerDecodeFromBase64AVX512Url(source, dest, out bytesConsumed, out bytesWritten); + } + else + { + return InnerDecodeFromBase64AVX512Regular(source, dest, out bytesConsumed, out bytesWritten); + } + } + + private unsafe static OperationStatus InnerDecodeFromBase64AVX512Regular(ReadOnlySpan source, Span dest, out int bytesConsumed, out int bytesWritten) + { + // translation from ASCII to 6 bit values + bool isUrl = false; + bytesConsumed = 0; + bytesWritten = 0; + const int blocksSize = 6; + // Should be + // Span buffer = stackalloc byte[blocksSize * 64]; + Span buffer = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + // Define pointers within the fixed blocks + fixed (byte* srcInit = source) + fixed (byte* dstInit = dest) + fixed (byte* startOfBuffer = buffer) + { + byte* srcEnd = srcInit + source.Length; + byte* src = srcInit; + byte* dst = dstInit; + byte* dstEnd = dstInit + dest.Length; + + int whiteSpaces = 0; + int equalsigns = 0; + + int bytesToProcess = source.Length; + // skip trailing spaces + while (bytesToProcess > 0 && SimdBase64.Scalar.Base64.IsAsciiWhiteSpace((char)source[bytesToProcess - 1])) + { + bytesToProcess--; + whiteSpaces++; + } + + int equallocation = bytesToProcess; // location of the first padding character if any + if (bytesToProcess > 0 && source[bytesToProcess - 1] == '=') + { + bytesToProcess -= 1; + equalsigns++; + while (bytesToProcess > 0 && SimdBase64.Scalar.Base64.IsAsciiWhiteSpace((char)source[bytesToProcess - 1])) + { + bytesToProcess--; + whiteSpaces++; + } + if (bytesToProcess > 0 && source[bytesToProcess - 1] == '=') + { + equalsigns++; + bytesToProcess -= 1; + } + } + + + { + byte* bufferPtr = startOfBuffer; + + ulong bufferBytesConsumed = 0;//Only used if there is an error + ulong bufferBytesWritten = 0;//Only used if there is an error + + if (bytesToProcess >= 64) + { + byte* srcEnd64 = srcInit + bytesToProcess - 64; + while (src <= srcEnd64) + { + + Base64.Block64 b; + Base64.LoadBlock(&b, src); + src += 64; + bufferBytesConsumed += 64; + bool error = false; + UInt64 badCharMask = Base64.ToBase64Mask(isUrl, &b, ref error); + if (error == true) + { + src -= bufferBytesConsumed; + dst -= bufferBytesWritten; + + bytesConsumed = Math.Max(0, (int)(src - srcInit)); + bytesWritten = Math.Max(0, (int)(dst - dstInit)); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + if (badCharMask != 0) + { + // optimization opportunity: check for simple masks like those made of + // continuous 1s followed by continuous 0s. And masks containing a + // single bad character. + ulong compressedBytesCount = CompressBlock(ref b, badCharMask, bufferPtr); + bufferPtr += compressedBytesCount; + bufferBytesConsumed += compressedBytesCount; + + } + else if (bufferPtr != startOfBuffer) + { + CopyBlock(&b, bufferPtr); + bufferPtr += 64; + bufferBytesConsumed += 64; + } + else + { + Base64DecodeBlock(dst, &b); + bufferBytesWritten += 48; + dst += 48; + } + + if (bufferPtr >= (blocksSize - 1) * 64 + startOfBuffer) // We treat the last block separately later on + { + for (int i = 0; i < (blocksSize - 2); i++) // We also treat the second to last block differently! Until then it is safe to proceed: + { + Base64DecodeBlock(dst, startOfBuffer + i * 64); + bufferBytesWritten += 48; + dst += 48; + } + Base64DecodeBlock(dst, startOfBuffer + (blocksSize - 2) * 64); + + dst += 48; + Buffer.MemoryCopy(startOfBuffer + (blocksSize - 1) * 64, startOfBuffer, 64, 64); + bufferPtr -= (blocksSize - 1) * 64; + + bufferBytesWritten = 0; + bufferBytesConsumed = 0; + } + + } + } + // Optimization note: if this is almost full, then it is worth our + // time, otherwise, we should just decode directly. + + int lastBlock = (int)((bufferPtr - startOfBuffer) % 64); + int lastBlockSrcCount = 0; + // There is at some bytes remaining beyond the last 64 bit block remaining + if (lastBlock != 0 && srcEnd - src + lastBlock >= 64) // We first check if there is any error and eliminate white spaces?: + { + while ((bufferPtr - startOfBuffer) % 64 != 0 && src < srcEnd) + { + byte val = SimdBase64.Tables.GetToBase64Value((uint)*src); + *bufferPtr = val; + if (val > 64) + { + bytesConsumed = Math.Max(0, (int)(src - srcInit) - lastBlockSrcCount - (int)bufferBytesConsumed); + bytesWritten = Math.Max(0, (int)(dst - dstInit) - (int)bufferBytesWritten); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + bufferPtr += (val <= 63) ? 1 : 0; + src++; + lastBlockSrcCount++; + } + + } + + byte* subBufferPtr = startOfBuffer; + for (; subBufferPtr + 64 <= bufferPtr; subBufferPtr += 64) + { + + Base64DecodeBlock(dst, subBufferPtr); + dst += 48; // 64 bits of base64 decodes to 48 bits + } + if ((bufferPtr - subBufferPtr) % 64 != 0) + { + while (subBufferPtr + 4 < bufferPtr) // we decode one base64 element (4 bit) at a time + { + + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 4, 4); + dst += 3; + subBufferPtr += 4; + } + if (subBufferPtr + 4 <= bufferPtr) // this may be the very last element, might be incomplete + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 3, 3); + dst += 3; + subBufferPtr += 4; + } + int leftover = (int)(bufferPtr - subBufferPtr); + if (leftover > 0) + { + while (leftover < 4 && src < srcEnd) + { + byte val = SimdBase64.Tables.GetToBase64Value((uint)*src); + if (val > 64) + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.InvalidData; + } + subBufferPtr[leftover] = (byte)(val); + leftover += (val <= 63) ? 1 : 0; + src++; + } + + if (leftover == 1) + { + + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.NeedMoreData; + } + if (leftover == 2) + { + UInt32 triple = ((UInt32)(subBufferPtr[0]) << 3 * 6) + + ((UInt32)(subBufferPtr[1]) << 2 * 6); + triple = BinaryPrimitives.ReverseEndianness(triple); + triple >>= 8; + Buffer.MemoryCopy(&triple, dst, 1, 1); + dst += 1; + } + else if (leftover == 3) + { + UInt32 triple = ((UInt32)(subBufferPtr[0]) << 3 * 6) + + ((UInt32)(subBufferPtr[1]) << 2 * 6) + + ((UInt32)(subBufferPtr[2]) << 1 * 6); + triple = BinaryPrimitives.ReverseEndianness(triple); + + triple >>= 8; + Buffer.MemoryCopy(&triple, dst, 2, 2); + dst += 2; + } + else + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 3, 3); + dst += 3; + } + } + } + + if (src < srcEnd + equalsigns) // We finished processing 64-bit blocks, we're not quite at the end yet + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.DecodeFromBase64Scalar(source.Slice(bytesConsumed, bytesToProcess - bytesConsumed), dest.Slice(bytesWritten), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + if (result == OperationStatus.InvalidData) + { + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + else + { + bytesConsumed += remainderBytesConsumed + (source.Length - bytesToProcess); + bytesWritten += remainderBytesWritten; + } + if (result == OperationStatus.Done && equalsigns > 0) + { + + // additional checks + if ((remainderBytesWritten % 3 == 0) || ((remainderBytesWritten % 3) + 1 + equalsigns != 4)) + { + result = OperationStatus.InvalidData; + } + } + return result; + } + if (equalsigns > 0) // final additional check + { + if (((int)(dst - dstInit) % 3 == 0) || (((int)(dst - dstInit) % 3) + 1 + equalsigns != 4)) + { + return OperationStatus.InvalidData; + } + } + + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.Done; + } + + } + } + + private unsafe static OperationStatus InnerDecodeFromBase64AVX512Url(ReadOnlySpan source, Span dest, out int bytesConsumed, out int bytesWritten) + { + // translation from ASCII to 6 bit values + bool isUrl = true; + bytesConsumed = 0; + bytesWritten = 0; + const int blocksSize = 6; + // Should be + // Span buffer = stackalloc byte[blocksSize * 64]; + Span buffer = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + // Define pointers within the fixed blocks + fixed (byte* srcInit = source) + fixed (byte* dstInit = dest) + fixed (byte* startOfBuffer = buffer) + { + byte* srcEnd = srcInit + source.Length; + byte* src = srcInit; + byte* dst = dstInit; + byte* dstEnd = dstInit + dest.Length; + + int whiteSpaces = 0; + int equalsigns = 0; + + int bytesToProcess = source.Length; + // skip trailing spaces + while (bytesToProcess > 0 && SimdBase64.Scalar.Base64.IsAsciiWhiteSpace((char)source[bytesToProcess - 1])) + { + bytesToProcess--; + whiteSpaces++; + } + + int equallocation = bytesToProcess; // location of the first padding character if any + if (bytesToProcess > 0 && source[bytesToProcess - 1] == '=') + { + bytesToProcess -= 1; + equalsigns++; + while (bytesToProcess > 0 && SimdBase64.Scalar.Base64.IsAsciiWhiteSpace((char)source[bytesToProcess - 1])) + { + bytesToProcess--; + whiteSpaces++; + } + if (bytesToProcess > 0 && source[bytesToProcess - 1] == '=') + { + equalsigns++; + bytesToProcess -= 1; + } + } + + + { + byte* bufferPtr = startOfBuffer; + + ulong bufferBytesConsumed = 0;//Only used if there is an error + ulong bufferBytesWritten = 0;//Only used if there is an error + + if (bytesToProcess >= 64) + { + byte* srcEnd64 = srcInit + bytesToProcess - 64; + while (src <= srcEnd64) + { + Base64.Block64 b; + Base64.LoadBlock(&b, src); + src += 64; + bufferBytesConsumed += 64; + bool error = false; + UInt64 badCharMask = Base64.ToBase64Mask(isUrl, &b, ref error); + if (error == true) + { + src -= bufferBytesConsumed; + dst -= bufferBytesWritten; + + bytesConsumed = Math.Max(0, (int)(src - srcInit)); + bytesWritten = Math.Max(0, (int)(dst - dstInit)); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + if (badCharMask != 0) + { + // optimization opportunity: check for simple masks like those made of + // continuous 1s followed by continuous 0s. And masks containing a + // single bad character. + ulong compressedBytesCount = CompressBlock(ref b, badCharMask, bufferPtr); + bufferPtr += compressedBytesCount; + bufferBytesConsumed += compressedBytesCount; + + + } + else if (bufferPtr != startOfBuffer) + { + CopyBlock(&b, bufferPtr); + bufferPtr += 64; + bufferBytesConsumed += 64; + } + else + { + Base64DecodeBlock(dst, &b); + bufferBytesWritten += 48; + dst += 48; + } + + if (bufferPtr >= (blocksSize - 1) * 64 + startOfBuffer) // We treat the last block separately later on + { + for (int i = 0; i < (blocksSize - 2); i++) // We also treat the second to last block differently! Until then it is safe to proceed: + { + Base64DecodeBlock(dst, startOfBuffer + i * 64); + bufferBytesWritten += 48; + dst += 48; + } + Base64DecodeBlock(dst, startOfBuffer + (blocksSize - 2) * 64); + + + + dst += 48; + Buffer.MemoryCopy(startOfBuffer + (blocksSize - 1) * 64, startOfBuffer, 64, 64); + bufferPtr -= (blocksSize - 1) * 64; + + bufferBytesWritten = 0; + bufferBytesConsumed = 0; + } + + } + } + // Optimization note: if this is almost full, then it is worth our + // time, otherwise, we should just decode directly. + int lastBlock = (int)((bufferPtr - startOfBuffer) % 64); + // There is at some bytes remaining beyond the last 64 bit block remaining + if (lastBlock != 0 && srcEnd - src + lastBlock >= 64) // We first check if there is any error and eliminate white spaces?: + { + int lastBlockSrcCount = 0; + while ((bufferPtr - startOfBuffer) % 64 != 0 && src < srcEnd) + { + byte val = Tables.GetToBase64UrlValue((byte)*src); + *bufferPtr = val; + if (val > 64) + { + bytesConsumed = Math.Max(0, (int)(src - srcInit) - lastBlockSrcCount - (int)bufferBytesConsumed); + bytesWritten = Math.Max(0, (int)(dst - dstInit) - (int)bufferBytesWritten); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.Base64WithWhiteSpaceToBinaryScalar(source.Slice(Math.Max(0, bytesConsumed)), dest.Slice(Math.Max(0, bytesWritten)), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + bufferPtr += (val <= 63) ? 1 : 0; + src++; + lastBlockSrcCount++; + } + } + + byte* subBufferPtr = startOfBuffer; + for (; subBufferPtr + 64 <= bufferPtr; subBufferPtr += 64) + { + Base64DecodeBlock(dst, subBufferPtr); + + dst += 48;// 64 bits of base64 decodes to 48 bits + } + if ((bufferPtr - subBufferPtr) % 64 != 0) + { + while (subBufferPtr + 4 < bufferPtr) // we decode one base64 element (4 bit) at a time + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 4, 4); + dst += 3; + subBufferPtr += 4; + } + if (subBufferPtr + 4 <= bufferPtr) // this may be the very last element, might be incomplete + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 3, 3); + dst += 3; + subBufferPtr += 4; + } + int leftover = (int)(bufferPtr - subBufferPtr); + if (leftover > 0) + { + + while (leftover < 4 && src < srcEnd) + { + byte val = Tables.GetToBase64UrlValue((byte)*src); + if (val > 64) + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.InvalidData; + } + subBufferPtr[leftover] = (byte)(val); + leftover += (val <= 63) ? 1 : 0; + src++; + } + + if (leftover == 1) + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.NeedMoreData; + } + if (leftover == 2) + { + UInt32 triple = ((UInt32)(subBufferPtr[0]) << 3 * 6) + + ((UInt32)(subBufferPtr[1]) << 2 * 6); + triple = BinaryPrimitives.ReverseEndianness(triple); + triple >>= 8; + Buffer.MemoryCopy(&triple, dst, 1, 1); + dst += 1; + } + else if (leftover == 3) + { + UInt32 triple = ((UInt32)(subBufferPtr[0]) << 3 * 6) + + ((UInt32)(subBufferPtr[1]) << 2 * 6) + + ((UInt32)(subBufferPtr[2]) << 1 * 6); + triple = BinaryPrimitives.ReverseEndianness(triple); + + triple >>= 8; + Buffer.MemoryCopy(&triple, dst, 2, 2); + dst += 2; + } + else + { + UInt32 triple = (((UInt32)((byte)(subBufferPtr[0])) << 3 * 6) + + ((UInt32)((byte)(subBufferPtr[1])) << 2 * 6) + + ((UInt32)((byte)(subBufferPtr[2])) << 1 * 6) + + ((UInt32)((byte)(subBufferPtr[3])) << 0 * 6)) + << 8; + triple = BinaryPrimitives.ReverseEndianness(triple); + Buffer.MemoryCopy(&triple, dst, 3, 3); + dst += 3; + } + } + } + + if (src < srcEnd + equalsigns) // We finished processing 64-bit blocks, we're not quite at the end yet + { + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + + int remainderBytesConsumed = 0; + int remainderBytesWritten = 0; + + OperationStatus result = + SimdBase64.Scalar.Base64.DecodeFromBase64Scalar(source.Slice(bytesConsumed, bytesToProcess - bytesConsumed), dest.Slice(bytesWritten), out remainderBytesConsumed, out remainderBytesWritten, isUrl); + + + if (result == OperationStatus.InvalidData) + { + bytesConsumed += remainderBytesConsumed; + bytesWritten += remainderBytesWritten; + return result; + } + else + { + bytesConsumed += remainderBytesConsumed + (source.Length - bytesToProcess); + bytesWritten += remainderBytesWritten; + } + if (result == OperationStatus.Done && equalsigns > 0) + { + + // additional checks + if ((remainderBytesWritten % 3 == 0) || ((remainderBytesWritten % 3) + 1 + equalsigns != 4)) + { + result = OperationStatus.InvalidData; + } + } + return result; + } + if (equalsigns > 0) // final additional check + { + if (((int)(dst - dstInit) % 3 == 0) || (((int)(dst - dstInit) % 3) + 1 + equalsigns != 4)) + { + return OperationStatus.InvalidData; + } + } + + bytesConsumed = (int)(src - srcInit); + bytesWritten = (int)(dst - dstInit); + return OperationStatus.Done; + } + + } + } + } + } +} diff --git a/src/SimdBase64.csproj b/src/SimdBase64.csproj index aa273f1..39c9260 100644 --- a/src/SimdBase64.csproj +++ b/src/SimdBase64.csproj @@ -2,7 +2,7 @@ Library - net9.0 + net10.0 enable true diff --git a/test/Base64DecodingTestsUTF16.cs b/test/Base64DecodingTestsUTF16.cs index 1e975a8..258444a 100644 --- a/test/Base64DecodingTestsUTF16.cs +++ b/test/Base64DecodingTestsUTF16.cs @@ -66,6 +66,13 @@ public void DecodeBase64CasesAvx2UTF16() DecodeBase64CasesUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void DecodeBase64CasesAvx512UTF16() + { + DecodeBase64CasesUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512); + } + protected static void CompleteDecodeBase64CasesUTF16(Base64WithWhiteSpaceToBinaryFromUTF16 Base64WithWhiteSpaceToBinaryFromUTF16, DecodeFromBase64DelegateSafeFromUTF16 DecodeFromBase64DelegateSafeFromUTF16) { List<(string decoded, string base64)> cases = new List<(string, string)> @@ -134,6 +141,13 @@ public void CompleteDecodeBase64CasesAvx2UTF16() CompleteDecodeBase64CasesUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void CompleteDecodeBase64CasesAvx512UTF16() + { + CompleteDecodeBase64CasesUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + protected static void MoreDecodeTestsUTF16(Base64WithWhiteSpaceToBinaryFromUTF16 Base64WithWhiteSpaceToBinaryFromUTF16, DecodeFromBase64DelegateSafeFromUTF16 DecodeFromBase64DelegateSafeFromUTF16) { @@ -213,6 +227,13 @@ public void MoreDecodeTestsAVX2UTF16() MoreDecodeTestsUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void MoreDecodeTestsAVX512UTF16() + { + MoreDecodeTestsUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + protected static void MoreDecodeTestsUrlUTF16(Base64WithWhiteSpaceToBinaryFromUTF16 Base64WithWhiteSpaceToBinaryFromUTF16, DecodeFromBase64DelegateSafeFromUTF16 DecodeFromBase64DelegateSafeFromUTF16) { if (Base64WithWhiteSpaceToBinaryFromUTF16 == null || DecodeFromBase64DelegateSafeFromUTF16 == null || SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar == null) @@ -289,6 +310,13 @@ public void MoreDecodeTestsUrlAvx2UTF16() MoreDecodeTestsUrlUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void MoreDecodeTestsUrlAvx512UTF16() + { + MoreDecodeTestsUrlUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + protected void RoundtripBase64UTF16(Base64WithWhiteSpaceToBinaryFromUTF16 Base64WithWhiteSpaceToBinaryFromUTF16, DecodeFromBase64DelegateSafeFromUTF16 DecodeFromBase64DelegateSafeFromUTF16) { for (int len = 0; len < 2048; len++) @@ -343,6 +371,13 @@ public void RoundtripBase64Avx2UTF16() RoundtripBase64UTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void RoundtripBase64Avx512UTF16() + { + RoundtripBase64UTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + protected void RoundtripBase64UrlUTF16(Base64WithWhiteSpaceToBinaryFromUTF16 Base64WithWhiteSpaceToBinaryFromUTF16, DecodeFromBase64DelegateSafeFromUTF16 DecodeFromBase64DelegateSafeFromUTF16) { if (Base64WithWhiteSpaceToBinaryFromUTF16 == null || DecodeFromBase64DelegateSafeFromUTF16 == null || SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar == null) @@ -394,6 +429,13 @@ public void RoundtripBase64UrlAVX2UTF16() RoundtripBase64UrlUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.SSE.Base64.DecodeFromBase64SSE); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void RoundtripBase64UrlAVX512UTF16() + { + RoundtripBase64UrlUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.SSE.Base64.DecodeFromBase64SSE); + } + protected static void BadPaddingBase64UTF16(Base64WithWhiteSpaceToBinaryFromUTF16 Base64WithWhiteSpaceToBinaryFromUTF16, DecodeFromBase64DelegateSafeFromUTF16 DecodeFromBase64DelegateSafeFromUTF16) { if (Base64WithWhiteSpaceToBinaryFromUTF16 == null || DecodeFromBase64DelegateSafeFromUTF16 == null || SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar == null) @@ -528,6 +570,13 @@ public void BadPaddingBase64Avx2UTF16() BadPaddingBase64UTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void BadPaddingBase64Avx512UTF16() + { + BadPaddingBase64UTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void BadPaddingUTF16Base64ARM() @@ -601,6 +650,13 @@ public void DoomedBase64RoundtripAvx2UTF16() DoomedBase64RoundtripUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void DoomedBase64RoundtripAvx512UTF16() + { + DoomedBase64RoundtripUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void DoomedBase64RoundtripARMUTF16() @@ -682,6 +738,13 @@ public void TruncatedDoomedBase64RoundtripAVX2UTF16() TruncatedDoomedBase64RoundtripUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void TruncatedDoomedBase64RoundtripAVX512UTF16() + { + TruncatedDoomedBase64RoundtripUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + protected void RoundtripBase64WithSpacesUTF16(Base64WithWhiteSpaceToBinaryFromUTF16 Base64WithWhiteSpaceToBinaryFromUTF16, DecodeFromBase64DelegateSafeFromUTF16 DecodeFromBase64DelegateSafeFromUTF16) { if (Base64WithWhiteSpaceToBinaryFromUTF16 == null || DecodeFromBase64DelegateSafeFromUTF16 == null || SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar == null) @@ -755,6 +818,13 @@ public void RoundtripBase64WithSpacesAvx2UTF16() RoundtripBase64WithSpacesUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void RoundtripBase64WithSpacesAvx512UTF16() + { + RoundtripBase64WithSpacesUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void RoundtripBase64WithSpacesARMUTF16() @@ -837,6 +907,13 @@ public void AbortedSafeRoundtripBase64AVX2UTF16() AbortedSafeRoundtripBase64UTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void AbortedSafeRoundtripBase64AVX512UTF16() + { + AbortedSafeRoundtripBase64UTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Sse)] [Trait("Category", "arm64")] @@ -921,6 +998,13 @@ public void AbortedSafeRoundtripBase64WithSpacesAVX2UTF16() AbortedSafeRoundtripBase64WithSpacesUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void AbortedSafeRoundtripBase64WithSpacesAVX512UTF16() + { + AbortedSafeRoundtripBase64WithSpacesUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void AbortedSafeRoundtripBase64WithSpacesARMUTF16() @@ -1007,6 +1091,13 @@ public void StreamingBase64RoundtripAvx2UTF16() StreamingBase64RoundtripUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void StreamingBase64RoundtripAvx512UTF16() + { + StreamingBase64RoundtripUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void StreamingBase64RoundtripARMUTF16() @@ -1092,6 +1183,13 @@ public void ReadmeTestAvx2UTF16() ReadmeTestUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void ReadmeTestAvx512UTF16() + { + ReadmeTestUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] @@ -1155,6 +1253,13 @@ public void ReadmeTestSafeAvx2UTF16() ReadmeTestSafeUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void ReadmeTestSafeAvx512UTF16() + { + ReadmeTestSafeUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void ReadmeTestSafeARMUTF16() @@ -1258,6 +1363,13 @@ public void DoomedBase64AtPos0Avx2UTF16() DoomedBase64AtPos0(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void DoomedBase64AtPos0Avx512UTF16() + { + DoomedBase64AtPos0(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void DoomedBase64AtPos0ARMUTF16() @@ -1318,6 +1430,13 @@ public void EnronFilesTestAvx2UTF16() EnronFilesTestUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void EnronFilesTestAvx512UTF16() + { + EnronFilesTestUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void EnronFilesTestARMUTF16() @@ -1374,6 +1493,13 @@ public void SwedenZoneBaseFileTestAvx2UTF16() SwedenZoneBaseFileTestUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void SwedenZoneBaseFileTestAvx512UTF16() + { + SwedenZoneBaseFileTestUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void SwedenZoneBaseFileTestARMUTF16() @@ -1466,6 +1592,13 @@ public void DoomedPartialBufferAvx2UTF16() DoomedPartialBufferUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void DoomedPartialBufferAvx512UTF16() + { + DoomedPartialBufferUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] [Trait("Category", "arm64")] public void DoomedPartialBufferARMUTF16() @@ -1572,6 +1705,13 @@ public void Issue511AVX2UTF16() Issue511UTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void Issue511AVX512UTF16() + { + Issue511UTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512); + } + [Trait("Category", "arm64")] [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] public void Issue511ARMUTF16() @@ -1647,6 +1787,13 @@ public void TruncatedCharErrorUTF16AVX2() TruncatedCharErrorUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void TruncatedCharErrorUTF16AVX512() + { + TruncatedCharErrorUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [Trait("Category", "arm64")] [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] public void TruncatedCharErrorUTF16ARM() @@ -1724,6 +1871,13 @@ public void TruncatedCharErrorUrlUTF16AVX2() TruncatedCharErrorUrlUTF16(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void TruncatedCharErrorUrlUTF16AVX512() + { + TruncatedCharErrorUrlUTF16(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace); + } + [Trait("Category", "arm64")] [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] public void TruncatedCharErrorUrlUTF16ARM() diff --git a/test/Base64DecodingTestsUTF8.cs b/test/Base64DecodingTestsUTF8.cs index c6adcc3..2235515 100644 --- a/test/Base64DecodingTestsUTF8.cs +++ b/test/Base64DecodingTestsUTF8.cs @@ -83,6 +83,13 @@ public void DecodeBase64CasesAvx2() DecodeBase64CasesUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void DecodeBase64CasesAvx512() + { + DecodeBase64CasesUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected static void CompleteDecodeBase64CasesUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { if (Base64WithWhiteSpaceToBinary == null || DecodeFromBase64DelegateSafe == null || MaxBase64ToBinaryLengthDelegate == null) @@ -164,6 +171,13 @@ public void CompleteDecodeBase64CasesAvx2() CompleteDecodeBase64CasesUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void CompleteDecodeBase64CasesAvx512() + { + CompleteDecodeBase64CasesUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected static void Issue511UTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary) { @@ -271,6 +285,13 @@ public void Issue511Avx2UTF8() Issue511UTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void Issue511Avx512UTF8() + { + Issue511UTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512); + } + protected static void MoreDecodeTestsUrlUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { if (Base64WithWhiteSpaceToBinary == null || DecodeFromBase64DelegateSafe == null || MaxBase64ToBinaryLengthDelegate == null) @@ -353,6 +374,13 @@ public void MoreDecodeTestsUrlAvx2UTF8() { MoreDecodeTestsUrlUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void MoreDecodeTestsUrlAvx512UTF8() + { + MoreDecodeTestsUrlUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } protected void RoundtripBase64UTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { if (Base64WithWhiteSpaceToBinary == null || DecodeFromBase64DelegateSafe == null || MaxBase64ToBinaryLengthDelegate == null) @@ -405,6 +433,13 @@ public void RoundtripBase64Avx2UTF8() RoundtripBase64UTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void RoundtripBase64Avx512UTF8() + { + RoundtripBase64UTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + [Trait("Category", "arm64")] [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] public void RoundtripBase64ARMUTF8() @@ -462,6 +497,13 @@ public void RoundtripBase64UrlAVX2UTF8() RoundtripBase64UrlUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.SSE.Base64.DecodeFromBase64SSE, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void RoundtripBase64UrlAVX512UTF8() + { + RoundtripBase64UrlUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.SSE.Base64.DecodeFromBase64SSE, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + [Trait("Category", "arm64")] [FactOnSystemRequirementAttribute(TestSystemRequirements.Arm64)] @@ -607,6 +649,13 @@ public void BadPaddingBase64Avx2UTF8() BadPaddingBase64UTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void BadPaddingBase64Avx512UTF8() + { + BadPaddingBase64UTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected void DoomedBase64Roundtrip(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { if (Base64WithWhiteSpaceToBinary == null || DecodeFromBase64DelegateSafe == null || MaxBase64ToBinaryLengthDelegate == null) @@ -679,6 +728,13 @@ public void DoomedBase64RoundtripAvx2UTF8() DoomedBase64Roundtrip(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void DoomedBase64RoundtripAvx512UTF8() + { + DoomedBase64Roundtrip(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected void TruncatedDoomedBase64Roundtrip(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { if (Base64WithWhiteSpaceToBinary == null || DecodeFromBase64DelegateSafe == null || MaxBase64ToBinaryLengthDelegate == null) @@ -752,6 +808,13 @@ public void TruncatedDoomedBase64RoundtripAVX2UTF8() TruncatedDoomedBase64Roundtrip(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void TruncatedDoomedBase64RoundtripAVX512UTF8() + { + TruncatedDoomedBase64Roundtrip(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected void RoundtripBase64WithSpacesUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { if (Base64WithWhiteSpaceToBinary == null || DecodeFromBase64DelegateSafe == null || MaxBase64ToBinaryLengthDelegate == null) @@ -833,6 +896,13 @@ public void RoundtripBase64WithSpacesAvx2UTF8() RoundtripBase64WithSpacesUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void RoundtripBase64WithSpacesAvx512UTF8() + { + RoundtripBase64WithSpacesUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected void AbortedSafeRoundtripBase64(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { if (Base64WithWhiteSpaceToBinary == null || DecodeFromBase64DelegateSafe == null || MaxBase64ToBinaryLengthDelegate == null) @@ -999,6 +1069,13 @@ public void AbortedSafeRoundtripBase64WithSpacesAVX2UTF8() AbortedSafeRoundtripBase64WithSpaces(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void AbortedSafeRoundtripBase64WithSpacesAVX512UTF8() + { + AbortedSafeRoundtripBase64WithSpaces(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected void StreamingBase64RoundtripUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { int len = 2048; @@ -1085,6 +1162,13 @@ public void StreamingBase64RoundtripAvx2UTF8() StreamingBase64RoundtripUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void StreamingBase64RoundtripAvx512UTF8() + { + StreamingBase64RoundtripUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected static void ReadmeTestUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { int len = 2048; @@ -1169,6 +1253,13 @@ public void ReadmeTestAvx2UTF8() ReadmeTestUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void ReadmeTestAvx512UTF8() + { + ReadmeTestUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected static void ReadmeTestSafeUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { int len = 72; @@ -1231,6 +1322,13 @@ public void ReadmeTestSafeAvx2UTF8() ReadmeTestSafeUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void ReadmeTestSafeAvx512UTF8() + { + ReadmeTestSafeUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected void DoomedBase64AtPos0UTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { if (Base64WithWhiteSpaceToBinary == null || DecodeFromBase64DelegateSafe == null || MaxBase64ToBinaryLengthDelegate == null) @@ -1315,6 +1413,13 @@ public void DoomedBase64AtPos0Avx2UTF8() DoomedBase64AtPos0UTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void DoomedBase64AtPos0Avx512UTF8() + { + DoomedBase64AtPos0UTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected static void EnronFilesTestUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { string[] fileNames = Directory.GetFiles("../../../../benchmark/data/email"); @@ -1376,6 +1481,13 @@ public void EnronFilesTestAvx2UTF8() EnronFilesTestUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void EnronFilesTestAvx512UTF8() + { + EnronFilesTestUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected static void EnronChoppedUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { string[] fileNames = Directory.GetFiles("../../../../benchmark/data/email"); @@ -1443,6 +1555,13 @@ public void EnronChoppedUTF8Avx2UTF8() EnronChoppedUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void EnronChoppedUTF8Avx512UTF8() + { + EnronChoppedUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected static void SwedenZoneBaseFileTestUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) @@ -1500,6 +1619,13 @@ public void SwedenZoneBaseFileTestAvx2UTF8() SwedenZoneBaseFileTestUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void SwedenZoneBaseFileTestAvx512UTF8() + { + SwedenZoneBaseFileTestUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + protected void DoomedPartialBufferUTF8(Base64WithWhiteSpaceToBinary Base64WithWhiteSpaceToBinary, DecodeFromBase64DelegateSafe DecodeFromBase64DelegateSafe, MaxBase64ToBinaryLengthDelegateFnc MaxBase64ToBinaryLengthDelegate) { byte[] VectorToBeCompressed = new byte[] { @@ -1585,6 +1711,13 @@ public void DoomedPartialBufferAvx2UTF8() DoomedPartialBufferUTF8(SimdBase64.AVX2.Base64.DecodeFromBase64AVX2, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); } + [Trait("Category", "avx512")] + [FactOnSystemRequirementAttribute(TestSystemRequirements.X64Avx512)] + public void DoomedPartialBufferAvx512UTF8() + { + DoomedPartialBufferUTF8(SimdBase64.AVX512.Base64.DecodeFromBase64AVX512, SimdBase64.Scalar.Base64.SafeBase64ToBinaryWithWhiteSpace, SimdBase64.Scalar.Base64.MaximalBinaryLengthFromBase64Scalar); + } + [Trait("Category", "arm64")] diff --git a/test/TestHelpers.cs b/test/TestHelpers.cs index 4e32894..fe6940d 100644 --- a/test/TestHelpers.cs +++ b/test/TestHelpers.cs @@ -154,7 +154,7 @@ private static bool IsSystemSupported(TestSystemRequirements requiredSystems) case Architecture.Arm64: return requiredSystems.HasFlag(TestSystemRequirements.Arm64) && AdvSimd.Arm64.IsSupported && BitConverter.IsLittleEndian; case Architecture.X64: - return (requiredSystems.HasFlag(TestSystemRequirements.X64Avx512) && Vector512.IsHardwareAccelerated && System.Runtime.Intrinsics.X86.Avx512F.IsSupported) || + return (requiredSystems.HasFlag(TestSystemRequirements.X64Avx512) && Avx512Vbmi2.IsSupported) || (requiredSystems.HasFlag(TestSystemRequirements.X64Avx2) && System.Runtime.Intrinsics.X86.Avx2.IsSupported) || (requiredSystems.HasFlag(TestSystemRequirements.X64Sse) && System.Runtime.Intrinsics.X86.Ssse3.IsSupported && Popcnt.IsSupported); default: diff --git a/test/tests.csproj b/test/tests.csproj index 860d5cc..607fb95 100644 --- a/test/tests.csproj +++ b/test/tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable From 5792d3021e92dbfa26db884544bfe5b8e58e27af Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Wed, 19 Aug 2026 12:21:22 -0400 Subject: [PATCH 2/2] Enable AVX2 and AVX-512 UTF-8 methods in the default benchmark set. This makes same-machine kernel comparisons part of the regular run. --- benchmark/Benchmark.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/benchmark/Benchmark.cs b/benchmark/Benchmark.cs index b26a5cd..c80fcd0 100644 --- a/benchmark/Benchmark.cs +++ b/benchmark/Benchmark.cs @@ -633,11 +633,15 @@ public unsafe void SSEDecodingRealDataWithAllocUTF8() RunSSEDecodingBenchmarkWithAllocUTF8(FileContent, DecodedLengths); } + [Benchmark] + [BenchmarkCategory("default")] public unsafe void AVX2DecodingRealDataUTF8() { RunAVX2DecodingBenchmarkUTF8(FileContent, DecodedLengths); } + [Benchmark] + [BenchmarkCategory("default")] public unsafe void AVX512DecodingRealDataUTF8() { RunAVX512DecodingBenchmarkUTF8(FileContent, DecodedLengths);