From c85460b0adb93841066c62914e384e0904d94029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sun, 14 Sep 2025 02:39:58 +0200 Subject: [PATCH] zlib: implement fast path for crc32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59813 Reviewed-By: Michaƫl Zasso Reviewed-By: Matteo Collina Reviewed-By: Daniel Lemire Reviewed-By: Trivikram Kamat Reviewed-By: James M Snell Reviewed-By: Ruben Bridgewater --- benchmark/zlib/crc32.js | 47 +++++++++++++++++++++ src/node_zlib.cc | 39 ++++++++++++----- test/sequential/test-zlib-crc32-fast-api.js | 26 ++++++++++++ 3 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 benchmark/zlib/crc32.js create mode 100644 test/sequential/test-zlib-crc32-fast-api.js diff --git a/benchmark/zlib/crc32.js b/benchmark/zlib/crc32.js new file mode 100644 index 0000000000..e70ee46afe --- /dev/null +++ b/benchmark/zlib/crc32.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common.js'); +const { crc32 } = require('zlib'); + +// Benchmark crc32 on Buffer and String inputs across sizes. +// Iteration count is scaled inversely with input length to keep runtime sane. +// Example: +// node benchmark/zlib/crc32.js type=buffer len=4096 n=4000000 +// ./out/Release/node benchmark/zlib/crc32.js --test + +const bench = common.createBenchmark(main, { + type: ['buffer', 'string'], + len: [32, 256, 4096, 65536], + n: [4e6], +}); + +function makeBuffer(size) { + const buf = Buffer.allocUnsafe(size); + for (let i = 0; i < size; i++) buf[i] = (i * 1103515245 + 12345) & 0xff; + return buf; +} + +function makeAsciiString(size) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + let out = ''; + for (let i = 0, j = 0; i < size; i++, j = (j + 7) % chars.length) out += chars[j]; + return out; +} + +function main({ type, len, n }) { + // Scale iterations so that total processed bytes roughly constant around n*4096 bytes. + const scale = 4096 / len; + const iters = Math.max(1, Math.floor(n * scale)); + + const data = type === 'buffer' ? makeBuffer(len) : makeAsciiString(len); + + let acc = 0; + for (let i = 0; i < Math.min(iters, 10000); i++) acc ^= crc32(data, 0); + + bench.start(); + let sum = 0; + for (let i = 0; i < iters; i++) sum ^= crc32(data, 0); + bench.end(iters); + + if (sum === acc - 1) process.stderr.write(''); +} diff --git a/src/node_zlib.cc b/src/node_zlib.cc index b8617093bd..c704e23db4 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -30,6 +30,8 @@ #include "threadpoolwork-inl.h" #include "util-inl.h" +#include "node_debug.h" +#include "v8-fast-api-calls.h" #include "v8.h" #include "brotli/decode.h" @@ -48,6 +50,7 @@ namespace node { using v8::ArrayBuffer; +using v8::CFunction; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; @@ -1657,22 +1660,35 @@ T CallOnSequence(v8::Isolate* isolate, Local value, F callback) { } } -// TODO(joyeecheung): use fast API +static inline uint32_t CRC32Impl(Isolate* isolate, + Local data, + uint32_t value) { + return CallOnSequence( + isolate, data, [&](const char* ptr, size_t size) -> uint32_t { + return static_cast( + crc32(value, reinterpret_cast(ptr), size)); + }); +} + static void CRC32(const FunctionCallbackInfo& args) { CHECK(args[0]->IsArrayBufferView() || args[0]->IsString()); CHECK(args[1]->IsUint32()); uint32_t value = args[1].As()->Value(); - - uint32_t result = CallOnSequence( - args.GetIsolate(), - args[0], - [&](const char* data, size_t size) -> uint32_t { - return crc32(value, reinterpret_cast(data), size); - }); - - args.GetReturnValue().Set(result); + args.GetReturnValue().Set(CRC32Impl(args.GetIsolate(), args[0], value)); } +static uint32_t FastCRC32(v8::Local receiver, + v8::Local data, + uint32_t value, + // NOLINTNEXTLINE(runtime/references) + v8::FastApiCallbackOptions& options) { + TRACK_V8_FAST_API_CALL("zlib.crc32"); + v8::HandleScope handle_scope(options.isolate); + return CRC32Impl(options.isolate, data, value); +} + +static CFunction fast_crc32_(CFunction::Make(FastCRC32)); + void Initialize(Local target, Local unused, Local context, @@ -1685,7 +1701,7 @@ void Initialize(Local target, MakeClass::Make(env, target, "ZstdCompress"); MakeClass::Make(env, target, "ZstdDecompress"); - SetMethod(context, target, "crc32", CRC32); + SetFastMethodNoSideEffect(context, target, "crc32", CRC32, &fast_crc32_); target->Set(env->context(), FIXED_ONE_BYTE_STRING(env->isolate(), "ZLIB_VERSION"), FIXED_ONE_BYTE_STRING(env->isolate(), ZLIB_VERSION)).Check(); @@ -1698,6 +1714,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { MakeClass::Make(registry); MakeClass::Make(registry); registry->Register(CRC32); + registry->Register(fast_crc32_); } } // anonymous namespace diff --git a/test/sequential/test-zlib-crc32-fast-api.js b/test/sequential/test-zlib-crc32-fast-api.js new file mode 100644 index 0000000000..37d5682a7e --- /dev/null +++ b/test/sequential/test-zlib-crc32-fast-api.js @@ -0,0 +1,26 @@ +// Flags: --expose-internals --no-warnings --allow-natives-syntax +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +{ + function testFastPath() { + const expected = 0xd87f7e0c; // zlib.crc32('test', 0) + assert.strictEqual(zlib.crc32('test', 0), expected); + return expected; + } + + eval('%PrepareFunctionForOptimization(zlib.crc32)'); + testFastPath(); + eval('%OptimizeFunctionOnNextCall(zlib.crc32)'); + testFastPath(); + testFastPath(); + + if (common.isDebug) { + const { internalBinding } = require('internal/test/binding'); + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual(getV8FastApiCallCount('zlib.crc32'), 2); + } +}