From faf81fdd382ee17b47d480760a9bdae84db31e96 Mon Sep 17 00:00:00 2001 From: Aran Reeks Date: Mon, 26 Oct 2026 07:01:31 +0000 Subject: [PATCH] ext/opcache: Use NEON for zend_adler32() on AArch64 On AArch64, zend_adler32() ran the SSE2 loop through zend_simd.h's SSE2-on-NEON layer, where _mm_madd_epi16() and _mm_sad_epu8() are slow. Use widening multiply-accumulates and pairwise adds instead. The checksum is unchanged, so existing file caches still verify. On an Apple M4 (Linux VM), checksumming 64MB goes from 12GB/s to 24GB/s with GCC 14 or Clang, and from 7.4GB/s to 24GB/s with GCC 11. Assisted-by: Claude --- UPGRADING | 3 + ext/opcache/tests/file_cache_checksum.phpt | 66 ++++++++++++++++++++++ ext/opcache/zend_accelerator_util_funcs.c | 29 +++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 ext/opcache/tests/file_cache_checksum.phpt diff --git a/UPGRADING b/UPGRADING index 49d1ebc8258c..e925ab7010e3 100644 --- a/UPGRADING +++ b/UPGRADING @@ -173,6 +173,9 @@ PHP 8.7 UPGRADE NOTES target does not keep is now released at that assignment instead of after the last one. +- Opcache: + . Improved performance of file cache checksum verification on AArch64. + - Standard: . Improved performance of strcspn() and strpbrk() when searching longer strings with a single-byte character mask. diff --git a/ext/opcache/tests/file_cache_checksum.phpt b/ext/opcache/tests/file_cache_checksum.phpt new file mode 100644 index 000000000000..e3ab3f3f8ab6 --- /dev/null +++ b/ext/opcache/tests/file_cache_checksum.phpt @@ -0,0 +1,66 @@ +--TEST-- +File cache checksum is the Adler-32 of the cached script +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.file_cache="{PWD}/file_cache_checksum_cache" +opcache.file_cache_only=1 +opcache.file_update_protection=0 +--EXTENSIONS-- +opcache +--FILE-- +getFilename(), 'file_cache_checksum.inc.bin')) { + $bin = file_get_contents($fileinfo->getPathname()); + } +} + +/* The header is zend_file_cache_metainfo (zend_file_cache.c): 80 bytes, with + * mem_size and str_size at offset 40 and the checksum of the rest at 72. */ +['mem' => $mem, 'str' => $str] = unpack('Qmem/Qstr', $bin, 40); +var_dump(strlen($bin) === 80 + $mem + $str && $mem + $str > 3 * 5552); +var_dump(sprintf('%08x', unpack('L', $bin, 72)[1]) === hash('adler32', substr($bin, 80))); + +?> +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); +} +removeDirRecursive(__DIR__ . '/file_cache_checksum_cache'); +@unlink(__DIR__ . '/file_cache_checksum.inc'); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/ext/opcache/zend_accelerator_util_funcs.c b/ext/opcache/zend_accelerator_util_funcs.c index 90496e456593..90fe6cb2ca44 100644 --- a/ext/opcache/zend_accelerator_util_funcs.c +++ b/ext/opcache/zend_accelerator_util_funcs.c @@ -457,7 +457,34 @@ static zend_always_inline void adler32_do16_loop(unsigned char *buf, const unsig unsigned int s1 = *s1_out; unsigned int s2 = *s2_out; -#ifdef XSSE2 +#if defined(__aarch64__) || defined(_M_ARM64) + /* zend_simd.h maps the SSE2 loop below to NEON too, but its _mm_madd_epi16() + * and _mm_sad_epu8() are slow there, so use NEON directly. + * For each 16-byte block: s2 += 16 * s1 + sum((16 - i) * buf[i]), then + * s1 += sum(buf[i]). The weighted products fit in 16 bits + * (255 * 16 + 255 * 8), and all 32-bit sums wrap like the scalar code's, so + * the result is identical for any input. */ + static const uint8_t weights[16] = {16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + const uint8x8_t weights_lo = vld1_u8(weights); + const uint8x8_t weights_hi = vld1_u8(weights + 8); + const unsigned int blocks = (unsigned int) ((end - buf) / 16); + uint32x4_t weighted_sum = vdupq_n_u32(0); + uint32x4_t byte_sum = vdupq_n_u32(0); + uint32x4_t byte_sum_at_block_start = vdupq_n_u32(0); + + do { + const uint8x16_t read = vld1q_u8(buf); + byte_sum_at_block_start = vaddq_u32(byte_sum_at_block_start, byte_sum); + uint16x8_t products = vmull_u8(vget_low_u8(read), weights_lo); + products = vmlal_u8(products, vget_high_u8(read), weights_hi); + weighted_sum = vpadalq_u16(weighted_sum, products); + byte_sum = vpadalq_u16(byte_sum, vpaddlq_u8(read)); + buf += 16; + } while (buf != end); + + s2 += 16 * (blocks * s1 + vaddvq_u32(byte_sum_at_block_start)) + vaddvq_u32(weighted_sum); + s1 += vaddvq_u32(byte_sum); +#elif defined(XSSE2) const __m128i zero = _mm_setzero_si128(); __m128i accumulate_s2 = zero;