Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions benchmark/Benchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++)
Expand Down Expand Up @@ -615,11 +633,20 @@ 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);
}

[Benchmark]
[BenchmarkCategory("default")]
public unsafe void SimdBase64DecodingRealDataUTF8()
Expand Down
2 changes: 1 addition & 1 deletion benchmark/benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand Down
4 changes: 3 additions & 1 deletion docs/articles/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/articles/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions docs/articles/getting-started.md
Original file line number Diff line number Diff line change
@@ -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 &amp; reference
Expand Down Expand Up @@ -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).
18 changes: 10 additions & 8 deletions docs/articles/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`?

Expand Down
2 changes: 1 addition & 1 deletion docs/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"memberLayout": "separatePages",
"enumSortOrder": "declaringOrder",
"properties": {
"TargetFramework": "net9.0"
"TargetFramework": "net10.0"
}
}
],
Expand Down
19 changes: 10 additions & 9 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ title: SimdBase64 — fast base64 decoding for .NET
<div class="hero">
<div class="hero-inner">
<h1 class="hero-title">SimdBase64</h1>
<p class="hero-tagline">A blazing-fast C# library for WHATWG forgiving-base64 decoding — <strong>up to&nbsp;2.3&times; faster</strong> than the accelerated .NET functions and <strong>3.8&times;</strong> faster than <code>Convert.FromBase64String</code>, using AVX2, SSE and ARM&nbsp;NEON.</p>
<p class="hero-tagline">A blazing-fast C# library for WHATWG forgiving-base64 decoding — <strong>up to&nbsp;2.6&times; faster</strong> than the accelerated .NET functions and <strong>3.8&times;</strong> faster than <code>Convert.FromBase64String</code>, using AVX-512, AVX2, SSE and ARM&nbsp;NEON.</p>
<div class="hero-cta">
<a class="btn btn-primary" href="articles/getting-started.md">Get started &rarr;</a>
<a class="btn btn-ghost" href="api/index.md">API reference</a>
Expand All @@ -25,8 +25,8 @@ title: SimdBase64 — fast base64 decoding for .NET
<div class="stat-label">faster than <code>Convert.FromBase64String</code></div>
</div>
<div class="stat-card">
<div class="stat-num">3</div>
<div class="stat-label">SIMD back-ends: AVX2, SSE4.2, NEON</div>
<div class="stat-num">4</div>
<div class="stat-label">SIMD back-ends: AVX-512, AVX2, SSE4.2, NEON</div>
</div>
<div class="stat-card">
<div class="stat-num">0</div>
Expand Down Expand Up @@ -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.

<div class="feature-grid">
<div class="feature">
Expand All @@ -73,7 +73,7 @@ The right SIMD kernel is selected automatically at runtime: **ARM64 NEON**, **AV
<div class="feature">
<div class="feature-icon">🧭</div>
<h3>Runtime dispatch</h3>
<p>One call, the best available kernel. AVX2, SSE4.2, ARM NEON or a scalar fallback — chosen for your CPU.</p>
<p>One call, the best available kernel. AVX-512, AVX2, SSE4.2, ARM NEON or a scalar fallback — chosen for your CPU.</p>
</div>
<div class="feature">
<div class="feature-icon">🧹</div>
Expand All @@ -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.

<div class="bench" data-unit="GB/s">
<div class="bench-row"><span class="bench-name">Apple M2 (NEON)</span><div class="bench-bars"><div class="bar bar-simd" style="--v:100%"><span>10 GB/s</span></div><div class="bar bar-net" style="--v:38%"><span>3.8</span></div></div><span class="bench-x">2.6&times;</span></div>
<div class="bench-row"><span class="bench-name">Intel Ice Lake</span><div class="bench-bars"><div class="bar bar-simd" style="--v:76%"><span>7.6 GB/s</span></div><div class="bar bar-net" style="--v:34%"><span>3.4</span></div></div><span class="bench-x">2.2&times;</span></div>
<div class="bench-row"><span class="bench-name">AMD EPYC (Zen 2)</span><div class="bench-bars"><div class="bar bar-simd" style="--v:69%"><span>6.9 GB/s</span></div><div class="bar bar-net" style="--v:30%"><span>3.0</span></div></div><span class="bench-x">2.3&times;</span></div>
<div class="bench-row"><span class="bench-name">AWS Graviton 3</span><div class="bench-bars"><div class="bar bar-simd" style="--v:51%"><span>5.1 GB/s</span></div><div class="bar bar-net" style="--v:20%"><span>2.0</span></div></div><span class="bench-x">2.6&times;</span></div>
<div class="bench-row"><span class="bench-name">Xeon Gold 6548N (AVX-512)</span><div class="bench-bars"><div class="bar bar-simd" style="--v:100%"><span>11.3 GB/s</span></div><div class="bar bar-net" style="--v:42%"><span>4.7</span></div></div><span class="bench-x">2.4&times;</span></div>
<div class="bench-row"><span class="bench-name">Apple M2 (NEON)</span><div class="bench-bars"><div class="bar bar-simd" style="--v:88%"><span>10 GB/s</span></div><div class="bar bar-net" style="--v:34%"><span>3.8</span></div></div><span class="bench-x">2.6&times;</span></div>
<div class="bench-row"><span class="bench-name">Intel Ice Lake (AVX2)</span><div class="bench-bars"><div class="bar bar-simd" style="--v:67%"><span>7.6 GB/s</span></div><div class="bar bar-net" style="--v:30%"><span>3.4</span></div></div><span class="bench-x">2.2&times;</span></div>
<div class="bench-row"><span class="bench-name">AMD EPYC (Zen 2)</span><div class="bench-bars"><div class="bar bar-simd" style="--v:61%"><span>6.9 GB/s</span></div><div class="bar bar-net" style="--v:27%"><span>3.0</span></div></div><span class="bench-x">2.3&times;</span></div>
<div class="bench-row"><span class="bench-name">AWS Graviton 3</span><div class="bench-bars"><div class="bar bar-simd" style="--v:45%"><span>5.1 GB/s</span></div><div class="bar bar-net" style="--v:18%"><span>2.0</span></div></div><span class="bench-x">2.6&times;</span></div>
</div>

<p class="bench-note">Against the unaccelerated <code>Convert.FromBase64String</code>, the gap is even larger — 3.6&times;–3.8&times;. See the full set of measurements in the <a href="articles/benchmarks.md">benchmarks</a>.</p>
Expand Down
17 changes: 8 additions & 9 deletions src/Base64.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ public unsafe static OperationStatus DecodeFromBase64(ReadOnlySpan<byte> 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);
Expand All @@ -56,11 +56,10 @@ public unsafe static OperationStatus DecodeFromBase64(ReadOnlySpan<char> 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);
Expand Down
Loading
Loading