Skip to content

ext/standard: Add SSE2/NEON fast path for bin2hex()/hex2bin() - #23789

Open
adapik wants to merge 3 commits into
php:masterfrom
adapik:binhex-simd
Open

adapik wants to merge 3 commits into
php:masterfrom
adapik:binhex-simd

Conversation

@adapik

@adapik adapik commented Sep 20, 2026

Copy link
Copy Markdown

Summary

bin2hex()/hex2bin() currently run a plain byte-by-byte scalar loop. This adds a SIMD fast path (zend_simd.h: SSE2 on x86-64, NEON on aarch64) that processes 16 bytes at a time, falling back to the existing scalar loop for tails and, in hex2bin, for any chunk containing an invalid hex character — so error handling and output stay byte-for-byte identical to before.

Who benefits: anything hex-encoding/decoding hashes, checksums, UUIDs, MAC addresses, tokens, or signatures — hash()/md5()/sha1() formatting, crypto/signature verification, logging, cache/session keys. This is the dominant real-world call shape for both functions.

Precedent: vectorizing hot byte/string functions via this header is established practice — stripslashes SSE2, bcmath ported to zend_simd.h, SSE2 mb_strlen, zend_simd.h itself.

Benchmarks

Real hardware, phpt correctness gate passing (ALL OK) before timing, on both hosts:

Linux x86-64 macOS arm64
CPU AMD Ryzen 5 7500F Apple M2 Silicon, 10 cores
SIMD path SSE2 NEON

Crypto/hash-sized inputs

Size bin2hex x86 bin2hex arm64 hex2bin x86 hex2bin arm64
4B (CRC32) ~1.0× ~1.0× ~1.0× ~1.0×
16B (MD5/UUID) 1.53× 1.34× 2.97× 2.15×
32B (SHA-256) 1.98× 1.76× 4.06× 2.92×
64B (SHA-512) 2.93× 2.38× 5.04× 3.65×
256B (RSA-2048 sig) 5.46× 4.56× 6.38× 4.87×
512B (RSA-4096 sig) 6.86× 5.46× 6.85× 4.98×

4B/6B show no win on either function — both are below the size where the SIMD path even engages (16 bytes for bin2hex, one full 32-char chunk for hex2bin), so those calls run the unmodified scalar loop. Not a regression, nothing changed for them.

Full corpus (throughput, branch/patched vs baseline/unpatched)

Size bin2hex x86 bin2hex arm64 hex2bin x86 hex2bin arm64
1KB–512KB up to 9.9× (20.6 GB/s) up to 6.8× up to 7.3× up to 5.7×
1MB–16MB 1.4–1.6× 2.6–3.0× 3.7–7.3× 3.6–5.1×

Allocator-boundary step-down, not a SIMD regression

strace on x86, 20 calls each: crossing the ~2MB output-buffer threshold takes bin2hex from 6 munmap/24 mmap calls to 65/63 — the allocator switches from heap reuse to a fresh mmap/munmap pair almost every call. That fixed syscall cost is the same for branch and baseline, but eats a much bigger share of the branch's now-tiny per-call time, which is why the throughput ratio compresses from ~10× down to 1.4–3× above 1MB rather than the branch getting slower in absolute terms.

@adapik adapik changed the title Zend/standard: Add SSE2/NEON fast path for bin2hex()/hex2bin() ext/standard: Add SSE2/NEON fast path for bin2hex()/hex2bin() Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant