Skip to content

Encode large Base64Url inputs with the vectorized encoder on net8 - #721

Merged
SimonCropp merged 1 commit into
mainfrom
perf/base64url-bulk-encode
Oct 12, 2026
Merged

SimonCropp merged 1 commit into
mainfrom
perf/base64url-bulk-encode

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

Follow-up to #683, which gave Base64Url a span-to-span encode loop. On net8 the BCL can do the bulk of that work vectorized, so for inputs of 192 bytes or more the complete 3-byte groups now go through Convert.TryToBase64Chars / Base64.EncodeToUtf8, straight into the destination. Complete groups encode without padding, so only + and / are left to change, which two vectorized Span.Replace passes do in place. The existing loop then encodes the last one or two bytes.

This is net8 only (the type is in the BCL from net9):

  • below net8 there is no vectorized Replace, and changing the alphabet with a loop costs as much as encoding with the loop does; measured on the net6.0 build, that was slower than what is there now
  • below about 190 bytes the fixed cost of the three calls outweighs the gain, so smaller inputs keep going straight to the loop. The loop is left in a method of its own (EncodeLoop) so that its code is not affected; on other targets nothing changes at all
  • TryEncodeToUtf8InPlace is left alone: going through Base64.EncodeToUtf8InPlace measured the same as the loop

Results are unchanged.

Tests

On top of the existing ones (every overload for every length from 0 to 300 against a Convert based reference, and a 100 KB input), a test for inputs that are nothing but +, nothing but /, or the two alternating in standard base-64, for every length from 0 to 400 (both sides of the threshold, every remainder): EncodeToString, EncodeToUtf8, EncodeToChars and TryEncodeToUtf8InPlace, with + and / placed right after the output to check that only the output is touched.

Passes against the previous implementation and against the real Base64Url on net9+.

Measurements

net8.0, Stopwatch / GC.GetAllocatedBytesForCurrentThread, Release, minimum of 5 alternating runs of before and after (each the best of 9 rounds after warm-up). Allocations are unchanged.

before after
EncodeToString(1KB) 1,947 ns 445 ns
EncodeToChars(1KB, dest) 664 ns 232 ns
EncodeToChars(1KB, dest, out, out) 660 ns 206 ns
EncodeToUtf8(1KB) 718 ns 200 ns
EncodeToUtf8(1KB, dest, out, out) 633 ns 130 ns
TryEncodeToUtf8(1KB, dest) 636 ns 130 ns
EncodeToString(16B) 27 ns 24 ns
EncodeToChars(17B, dest) 16 ns 16 ns
EncodeToChars(32B, dest) 25 ns 24 ns
EncodeToUtf8(32B) 30 ns 30 ns
TryEncodeToUtf8InPlace(1KB) 726 ns 670 ns

(The machine was busy; the rows for small inputs and for in-place run the same code before and after, so their differences are noise.)

@SimonCropp
SimonCropp force-pushed the perf/base64url-bulk-encode branch from 4ca067e to d3bb497 Compare October 12, 2026 22:22
@SimonCropp
SimonCropp merged commit 7ba75d6 into main Oct 12, 2026
2 checks passed
@SimonCropp
SimonCropp deleted the perf/base64url-bulk-encode branch October 12, 2026 22:24
This was referenced Oct 12, 2026
This was referenced Oct 13, 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