base64 scalar - Improve performance of scalar base64 decoder and encoder - #1037
Open
gaspardpetit wants to merge 8 commits into
Open
gaspardpetit wants to merge 8 commits into
gaspardpetit wants to merge 8 commits into
Conversation
Contributor
Author
|
I would hold this PR for now; a side effect here is that we improve the tail handling in SIMD implementations, but while performance improved for tails of >= 12 bytes, it degraded slightly for smaller ones. It turns out the extra setup increases stack allocation and prevents some register optimizations that were previously possible. I'll provide a better solution soon. |
gaspardpetit
marked this pull request as draft
September 19, 2026 01:56
…B in the alphabet lookup (simdutf#1038) Port of the lookup-index simplification from simdutf#1036 (AVX2) to the SSE encoder. Saturating-subtract 51, then subtract the (input > 25) mask (which is -1), so that the LUT index becomes 0 for 'A'..'Z', 1 for 'a'..'z', 2..11 for digits, 12 and 13 for the two symbols. Saves one instruction per 16 output bytes.
… cheaper alphabet lookup (simdutf#1039) - binary_to_base64_with_lines was falling back to the scalar code on both lsx and lasx; it now uses the vectorized encoders with the same line-feed insertion scheme as the westmere/haswell kernels. - lasx: groups 1..3 of each 96-byte block use a single overlapping 32-byte load instead of two 16-byte loads plus xvpermi.q (as in simdutf#1036). - lsx/lasx: vshuf.b/xvshuf.b only use the low five bits of the index, so the second table shuffle no longer subtracts 32 from the indices.
…rlap operations (simdutf#1036) * replace "compare + AND + OR" with "compare + SUB" / overlap operations * Applied clang-format 18.1.8
gaspardpetit
marked this pull request as ready for review
September 19, 2026 05:03
Open
11 tasks
Contributor
Author
|
PR is ready for review. The benefits are clear for the scalar-only implementation, and for SIMD, the extra branch cost is offset when combined with #1041 |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Short title (summary):
Accelerate scalar Base64 encoding and decoding
Description
Optimize the scalar Base64 clean-input paths while preserving existing API behavior. Encoding processes four 3-byte groups using 12-bit pair lookup tables; decoding processes three 4-character groups together with combined validation and a packed output store. Existing paths continue to handle tails, whitespace, garbage, URL-safe input, UTF-16, strict/partial modes, padding, and bounded output.
Type of change
How to verify / test
The existing
base64_testsandconstexpr_base64_testspass. A dedicated C++23 compile-time test covering the new encoder and decoder paths also passes.Scalar-only benchmarks were compiled with MSVC 19.51 using
/O2, with SIMD backends disabled. Each number is the average of the medians from two alternating original/patched runs, using seven samples of approximately 350 ms each. Throughput is measured against binary input for encoding and encoded input for decoding.The benchmark validates output correctness before measuring each case.
Checklist before submitting
Final notes
Further performance can be achieved if we reduce the responsibilities of the code, ex. externalize whitespace handling - see for example https://github.com/gaspardpetit/base64/
At this point, I maintained backward compatibility, but if interested, I can push this further.
[EDIT]
The original naive approach had an unexpected side effect - adding the new 12 bit handling branch added pressure on the registers and increased stack allocation, causing additional fixed cost on encoding of small payloads - in particular the 1-11 byte range.
The solution was to split the new branch in its own function - which still left a bit of degradation on small payloads (1-2%) for the extra branch.
I chose to integrate another PR I wanted to submit later with this one to make sure we improve all cases, and so I added size-based custom handling of tails. The net effect provides significant improvement on all small payloads sizes.
This improvement - although it primarily targets the scalar implementation - also benefits SIMD implementations that delegate the small tails (ex. < 28 bytes) to scalar handling. So these improvements are particularly beneficial to SIMD implementations when handling 1-256 bytes.
For encoding, I tested AVX2, but I expect similar results across the board for SIMD:
Similarly, for decoding, AVX2 degrades slightly (2ns) in the 1-8B because of the new branch, but then improves well in the 9-47 B range and the 57-95 B where we have 1 or 2 AVX2 blocks and enough tail bytes to follow the new branch. Meanwhile, pure scalar decoding is improved across the board.
(The +2% / -2% in the >1KiB is likely to be noise here - it is not supported by the changes being submitted).
I will be providing a third PR that will stack over this one and address the cliffs you have also been observing with SIMD implementations, there tail degrades until we reach the magic multiples.