Repository: nodejs/node
Pull request: zlib: implement fast path for crc32

Full benchmarks:

```sh
node % node benchmark/zlib/crc32.js
zlib/crc32.js n=4000000 len=32 type="buffer": 37,396,444.66876052
zlib/crc32.js n=4000000 len=256 type="buffer": 34,468,122.98396251
zlib/crc32.js n=4000000 len=4096 type="buffer": 4,642,218.20245069
zlib/crc32.js n=4000000 len=65536 type="buffer": 414,698.94918172585
zlib/crc32.js n=4000000 len=32 type="string": 27,544,938.95769763
zlib/crc32.js n=4000000 len=256 type="string": 13,360,556.974013995
zlib/crc32.js n=4000000 len=4096 type="string": 3,598,194.020364296
zlib/crc32.js n=4000000 len=65536 type="string": 247,640.72679581624
node % ./node benchmark/zlib/crc32.js
zlib/crc32.js n=4000000 len=32 type="buffer": 47,694,181.36925779
zlib/crc32.js n=4000000 len=256 type="buffer": 41,584,645.71789544
zlib/crc32.js n=4000000 len=4096 type="buffer": 6,730,071.896634588
zlib/crc32.js n=4000000 len=65536 type="buffer": 418,870.8874470888
zlib/crc32.js n=4000000 len=32 type="string": 34,844,878.72618591
zlib/crc32.js n=4000000 len=256 type="string": 24,282,737.956485238
zlib/crc32.js n=4000000 len=4096 type="string": 2,102,476.0393039584
zlib/crc32.js n=4000000 len=65536 type="string": 139,447.8455126307
```

Changed files:
filename: benchmark/zlib/crc32.js
patch: @@ -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('');
+}

filename: src/node_zlib.cc
patch: @@ -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> value, F callback) {
   }
 }
 
-// TODO(joyeecheung): use fast API
+static inline uint32_t CRC32Impl(Isolate* isolate,
+                                 Local<Value> data,
+                                 uint32_t value) {
+  return CallOnSequence<uint32_t>(
+      isolate, data, [&](const char* ptr, size_t size) -> uint32_t {
+        return static_cast<uint32_t>(
+            crc32(value, reinterpret_cast<const Bytef*>(ptr), size));
+      });
+}
+
 static void CRC32(const FunctionCallbackInfo<Value>& args) {
   CHECK(args[0]->IsArrayBufferView() || args[0]->IsString());
   CHECK(args[1]->IsUint32());
   uint32_t value = args[1].As<v8::Uint32>()->Value();
+  args.GetReturnValue().Set(CRC32Impl(args.GetIsolate(), args[0], value));
+}
 
-  uint32_t result = CallOnSequence<uint32_t>(
-      args.GetIsolate(),
-      args[0],
-      [&](const char* data, size_t size) -> uint32_t {
-        return crc32(value, reinterpret_cast<const Bytef*>(data), size);
-      });
-
-  args.GetReturnValue().Set(result);
+static uint32_t FastCRC32(v8::Local<v8::Value> receiver,
+                          v8::Local<v8::Value> 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<Object> target,
                 Local<Value> unused,
                 Local<Context> context,
@@ -1685,7 +1701,7 @@ void Initialize(Local<Object> target,
   MakeClass<ZstdCompressStream>::Make(env, target, "ZstdCompress");
   MakeClass<ZstdDecompressStream>::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<ZstdCompressStream>::Make(registry);
   MakeClass<ZstdDecompressStream>::Make(registry);
   registry->Register(CRC32);
+  registry->Register(fast_crc32_);
 }
 
 }  // anonymous namespace

filename: test/sequential/test-zlib-crc32-fast-api.js
patch: @@ -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);
+  }
+}