[php-src] PR #23789: Zend/standard: Add SSE2/NEON fast path for bin2hex()/hex2bin()

From: Date: Sun, 20 Sep 2026 14:05:53 +0000
Subject: [php-src] PR #23789: Zend/standard: Add SSE2/NEON fast path for bin2hex()/hex2bin()
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-38625@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/23789 Author: adapik # 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](https://github.com/php/php-src/commit/2b55dee4dccb695b7ae13be08efab6b147c698d3), [bcmath ported to zend_simd.h](https://github.com/php/php-src/commit/f142b6e6032c08b8fb8102fff8c7710390e68dbe), [SSE2 mb_strlen](https://github.com/php/php-src/pull/10257), [zend_simd.h itself](https://github.com/php/php-src/pull/18413). ## 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.

« previous php.git-pulls (#38625) next »