Skip to content

b64: speed up encode/decode with lookup tables#185

Merged
sergio-correia merged 1 commit into
latchset:masterfrom
hawk259:b64-speedup
Jul 23, 2026
Merged

b64: speed up encode/decode with lookup tables#185
sergio-correia merged 1 commit into
latchset:masterfrom
hawk259:b64-speedup

Conversation

@hawk259

@hawk259 hawk259 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Optimizes both base64url functions in lib/b64.c to be table-driven,
with no change in behavior or public API.

  • Decode (jose_b64_dec_buf): the inner loop did a linear scan over
    the 64-char alphabet for every input byte, plus a strlen(map) per
    call. Replaced with a single lookup into a 256-entry reverse table.
  • Encode (jose_b64_enc_buf): already used the forward map[]
    table, but branched per byte on io % 3 and wrote some output
    positions twice. Rewritten to process whole 3-byte blocks with a tail
    switch for the final 1–2 bytes.

Implementation notes

  • The reverse table is a compile-time static const uint8_t[256], so it
    requires no runtime init and is inherently thread-safe — relevant
    because tests/api_b64.c calls the decoder concurrently under OpenMP.
  • Designated-initializer entries default to 0, so valid values are
    stored as index + 1 and 0 is the "invalid character" sentinel.
  • The table must stay consistent with JOSE_B64_MAP; this is asserted
    in practice by the exhaustive decode test, which fails on any drift.

Behavior

Unchanged: unpadded base64url alphabet, strict validation of trailing
bits and invalid characters, and the same SIZE_MAX-on-error / NULL
output-sizing contract. No signature or header changes.

Performance

Microbenchmark, gcc -O2, 256 KiB pseudo-random input, per direction:

Direction Before After Speedup
Encode 0.40 GB/s 1.54 GB/s ~3.9×
Decode 0.02 GB/s 0.34 GB/s ~19×

Decode sees the large win because it goes from O(64)/byte to O(1)/byte;
encode is a constant-factor gain (branch + redundant-write removal).
These are isolated microbenchmarks of the two hot functions — end-to-end
speedup in JWE/JWS/JWK callers will be smaller.

Testing

  • meson test -C build api_b64 — the exhaustive test that round-trips
    every 1-, 2-, 3-, and 4-byte encoding and checks every invalid
    byte combination: pass.
  • meson test -C build — full suite 26/26 pass.
  • CLI round-trip sanity across all tail lengths (0/1/2 leftover bytes):
    aYQ, abYWI, abcYWJj, all decode back correctly.

…kups

The base64url decoder searched the 64-character alphabet linearly for
every input byte (~32 comparisons/byte on average) and recomputed
strlen(map) on every call. Replace the scan with a static 256-entry
reverse-lookup table (char -> 6-bit value), making the alphabet decode
a single O(1) load per byte.

The encoder already used the forward map[] table, but drove it with a
per-byte `switch (io % 3)` branch and a write-then-overwrite pattern
that touched some output positions twice. Rewrite it to consume whole
3-byte blocks (24 bits -> four map[] lookups) with a small tail switch
for the trailing 1 or 2 bytes, removing the per-byte branch and the
redundant writes.

The reverse table is a compile-time `static const`, so it needs no
initialization and is inherently thread-safe (tests/api_b64.c exercises
the decoder concurrently under OpenMP). Unspecified entries default to
0, so valid entries are stored as (index + 1) with 0 as the invalid
sentinel. Behavior is byte-for-byte unchanged: unpadded base64url,
strict validation, same SIZE_MAX error contract; no public API change.

Microbenchmark (gcc -O2, 256 KiB random input, this direction only):
  encode  0.40 -> 1.54 GB/s  (~3.9x)
  decode  0.02 -> 0.34 GB/s  (~19x)

tests/api_b64.c (exhaustive over all 1-, 2-, 3-, 4-byte encodings) and
the full suite (26/26) pass unchanged.

@simo5 simo5 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@sergio-correia sergio-correia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, LGTM

@sergio-correia
sergio-correia merged commit 1c809c8 into latchset:master Jul 23, 2026
22 checks passed
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.

3 participants