Skip to content

Use the span based Convert APIs in the Base64 polyfill - #688

Merged
SimonCropp merged 1 commit into
mainfrom
perf/base64-span-convert
Oct 12, 2026
Merged

SimonCropp merged 1 commit into
mainfrom
perf/base64-span-convert

Conversation

@SimonCropp

@SimonCropp SimonCropp commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

The Base64 polyfill (net11 members) encoded and decoded through source.ToArray() and intermediate strings/arrays on every target.

On NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER the helpers now use the span based BCL APIs:

  • EncodeToString -> Convert.ToBase64String(ReadOnlySpan<byte>)
  • EncodeToChars(source, destination) / TryEncodeToChars -> Convert.TryToBase64Chars straight into the destination
  • EncodeToChars(source) -> Convert.TryToBase64Chars straight into the result array
  • DecodeFromChars / TryDecodeFromChars / DecodeFromUtf8(source) -> Convert.TryFromBase64Chars into a stack (up to 256 bytes) or pooled buffer

On every target EncodeToUtf8(source) now calls Base64.EncodeToUtf8 straight into a right-sized array instead of building a string and narrowing it.

The contract is unchanged:

  • invalid input still throws FormatException. Convert.TryFromBase64Chars returns false for both invalid input and a destination that is too small, so the decode goes to a buffer of source.Length / 4 * 3 bytes (the most that valid input can produce); a false then can only mean invalid input, and the array overload is called to throw the same exception as before
  • a destination that is too small still returns false with 0 written (or throws ArgumentException from DecodeFromChars/EncodeToChars), invalid input still wins over a destination that is too small, and the destination is still only written to on success
  • whitespace is still skipped

Targets without the span based Convert APIs (net4x, netstandard2.0, netcoreapp2.0) keep the previous code for everything except EncodeToUtf8(source). The OperationStatus overloads are not touched here.

Tests

  • every changed overload against Convert for every length from 0 to 400 (all remainders, stack and pooled buffers), with a sentinel after the expected output
  • destinations one element too small for the Try and throwing overloads
  • empty and whitespace-only input, whitespace inside the input
  • invalid input (bad character, bad length, bad padding; short and long enough to take the pooled path) for all decode overloads, including together with a destination that is too small
  • non-ASCII bytes in DecodeFromUtf8

The real net11 Base64 reports partial progress when the destination is too small and reports that ahead of later invalid input; the tests check those two points only where the polyfill is in use (runtime check), everything else runs against both.

Measurements

Stopwatch / GC.GetAllocatedBytesForCurrentThread, Release, minimum of 4 alternating runs of before and after (each the best of 9 rounds after warm-up).

net10.0

before after
EncodeToString(32B) 30 ns / 168 B 25 ns / 112 B
EncodeToString(1KB) 222 ns / 3,808 B 189 ns / 2,760 B
EncodeToChars(32B, dest) 41 ns / 168 B 15 ns / 0 B
EncodeToChars(1KB, dest) 326 ns / 3,808 B 83 ns / 0 B
EncodeToChars(1KB) 443 ns / 6,568 B 230 ns / 2,760 B
TryEncodeToChars(1KB, dest) 327 ns / 3,808 B 148 ns / 0 B
EncodeToUtf8(32B) 45 ns / 240 B 16 ns / 72 B
EncodeToUtf8(1KB) 661 ns / 5,200 B 110 ns / 1,392 B
DecodeFromChars(32B) 46 ns / 168 B 40 ns / 56 B
DecodeFromChars(1KB) 1,018 ns / 3,808 B 676 ns / 1,048 B
DecodeFromChars(1KB, dest) 1,158 ns / 3,808 B 538 ns / 0 B
TryDecodeFromChars(1KB, dest) 1,060 ns / 3,808 B 537 ns / 0 B
DecodeFromUtf8(1KB) 1,273 ns / 3,808 B 1,176 ns / 1,048 B

net48 (only EncodeToUtf8(source) changes)

before after
EncodeToUtf8(32B) 205 ns / 248 B 63 ns / 72 B
EncodeToUtf8(1KB) 1,783 ns / 5,208 B 715 ns / 1,392 B

@SimonCropp SimonCropp added this to the 11.4.5 milestone Oct 12, 2026
@SimonCropp
SimonCropp merged commit 8904003 into main Oct 12, 2026
6 checks passed
@SimonCropp
SimonCropp deleted the perf/base64-span-convert branch October 12, 2026 12:06
This was referenced Oct 12, 2026
This was referenced Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant