Repository navigation
Conversation
|
I measured this on master c65321c (c4c2ad7 merges cleanly) with GCC 13.3, GCC 14.2, clang 18.1 and clang 20.1 on an i7-13700H, built with the flags of the official Docker image. On big-endian AArch64 Change in CPU cycles per call with the PR, relative to master (negative = fewer cycles, faster); median of five alternating runs:
Short Correctness on x86-64: output identical to master with all four compilers and under ASan/UBSan, on every length up to 300 bytes for Tests: in Full tables, scripts and the big-endian harness: https://github.com/ArtUkrainskiy/php-src-bench/tree/main/reports/pr-23789-bin2hex-hex2bin |
| static MUTEX_T locale_mutex = NULL; | ||
| #endif | ||
|
|
||
| #ifdef XSSE2 |
There was a problem hiding this comment.
On big-endian AArch64 this helper returns true for valid input but writes wrong bytes. zend_simd.h enables the NEON path for every __aarch64__ target, and php-src's configure handles aarch64_be. I cut this function out of the PR into a freestanding program, built it for aarch64_be-none-linux-gnu with clang 18.1 and 20.1 (-O2 and -O0) and ran it under qemu-aarch64_be 8.2.2. For all 50,000 random valid 32-character chunks it returns true and writes 16 zero bytes, for example:
in b25Db6b0507f311F7C5acBeBbD66FDe3
got 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Invalid chunks are still rejected, zend_bin2hex_simd() gives correct output there, and the same program built for little-endian AArch64 is correct. Harness and output: https://github.com/ArtUkrainskiy/php-src-bench/tree/main/reports/pr-23789-bin2hex-hex2bin#big-endian-aarch64
Excluding big-endian targets, as zend_inline_hash_func() does for its aarch64 code, avoids it:
| #ifdef XSSE2 | |
| #if defined(XSSE2) && !defined(WORDS_BIGENDIAN) |
|
|
||
| for (i = j = 0; i < target_length; i++) { | ||
| i = j = 0; | ||
| #ifdef XSSE2 |
There was a problem hiding this comment.
The same guard here, so that the loop is not compiled where the helper is not:
| #ifdef XSSE2 | |
| #if defined(XSSE2) && !defined(WORDS_BIGENDIAN) |
| } | ||
| echo implode(' ', $bytes), "\n"; | ||
|
|
||
| $hex = str_repeat('0123456789abcdefABCDEF', 10); |
There was a problem hiding this comment.
This string is 22 characters, so str_repeat(..., 10) gives 220 and the 2000 case below tests 220 characters:
| $hex = str_repeat('0123456789abcdefABCDEF', 10); | |
| $hex = str_repeat('0123456789abcdefABCDEF', 100); |
|
@ArtUkrainskiy thanks for the review, I found your comments reasonable and implemented the changes. Nice catch regarding endianness. |
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, inhex2bin, 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 —
stripslashesSSE2, bcmath ported tozend_simd.h, SSE2mb_strlen,zend_simd.hitself.Benchmarks
Crypto/hash-sized inputs
bin2hexx86bin2hexarm64hex2binx86hex2binarm644B/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 forhex2bin), so those calls run the unmodified scalar loop. Not a regression, nothing changed for them.Full corpus (throughput, branch/patched vs baseline/unpatched)
bin2hexx86bin2hexarm64hex2binx86hex2binarm64Allocator-boundary step-down, not a SIMD regression
straceon x86, 20 calls each: crossing the ~2MB output-buffer threshold takesbin2hexfrom 6munmap/24mmapcalls to 65/63 — the allocator switches from heap reuse to a freshmmap/munmappair 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.