Repository: denoland/deno
Pull request: perf(node): optimize Buffer compare

Implements the comparison part in native code, which effectively uses memcmp instead of comparing byte by byte

Benchmark:
```ts
import { bench, run } from "mitata";
import { Buffer } from "node:buffer";

const a = Buffer.from(new Array(8 * 1024 * 1024).fill(2));
const b = Buffer.from(new Array(8 * 1024 * 1024).fill(2));

bench("Buffer.compare offset", () => a.compare(b, 6, undefined, 10));

bench("Buffer.compare full", () => Buffer.compare(a, b));

await run();
```

Before:
```
❯ deno run -A compare.mjs
cpu: Apple M3 Max
runtime: deno 3.0.0-rc.0+0bd52c4 (aarch64-apple-darwin)

benchmark                   avg (min … max) p75 / p99    
------------------------------------------- ---------
Buffer.compare offset         14.95 ms/iter  15.22 ms
                      (14.29 ms … 17.19 ms)  16.83 ms  
                    (  1.09 kb …   9.75 kb)   1.64 kb 

Buffer.compare full           17.67 ms/iter  19.75 ms
                      (14.17 ms … 23.50 ms)  22.59 ms
                    (696.00  b … 776.00  b) 698.05  b
```

After:
```
❯ devdeno run -A compare.mjs
cpu: Apple M3 Max
runtime: deno 3.0.0-rc.0 (aarch64-apple-darwin)

benchmark                   avg (min … max) p75 / p99   
------------------------------------------- ---------
Buffer.compare offset        172.66 µs/iter 173.21 µs
                    (162.38 µs … 284.37 µs) 212.33 µs 
                    (  0.00  b … 352.40 kb)   1.03 kb

Buffer.compare full          167.45 µs/iter 167.33 µs
                    (151.21 µs … 266.63 µs) 224.79 µs 
                    (  0.00  b … 448.40 kb)   1.12 kb
```



Changed files:
filename: ext/node/lib.rs
patch: @@ -151,6 +151,8 @@ deno_core::extension!(deno_node,
     ops::buffer::op_is_ascii,
     ops::buffer::op_is_utf8,
     ops::buffer::op_transcode,
+    ops::buffer::op_node_buffer_compare,
+    ops::buffer::op_node_buffer_compare_offset,
     ops::constant::op_node_fs_constants,
     ops::buffer::op_node_decode_utf8,
     ops::crypto::op_node_check_prime_async,

filename: ext/node/ops/buffer.rs
patch: @@ -121,6 +121,49 @@ fn utf8_to_ascii(source: &[u8]) -> Vec<u8> {
   ascii_bytes
 }
 
+#[op2(fast)]
+#[smi]
+pub fn op_node_buffer_compare(
+  #[buffer] buf1: &[u8],
+  #[buffer] buf2: &[u8],
+) -> i32 {
+  buf1.cmp(buf2) as i32
+}
+
+#[op2(fast)]
+#[smi]
+pub fn op_node_buffer_compare_offset(
+  #[buffer] source: &[u8],
+  #[buffer] target: &[u8],
+  #[smi] source_start: usize,
+  #[smi] target_start: usize,
+  #[smi] source_end: usize,
+  #[smi] target_end: usize,
+) -> Result<i32, JsErrorBox> {
+  if source_start > source.len() {
+    return Err(JsErrorBox::from_err(BufferError::OutOfRangeNamed(
+      "sourceStart".to_string(),
+    )));
+  }
+  if target_start > target.len() {
+    return Err(JsErrorBox::from_err(BufferError::OutOfRangeNamed(
+      "targetStart".to_string(),
+    )));
+  }
+
+  if source_start > source_end {
+    panic!("source_start > source_end");
+  }
+  if target_start > target_end {
+    panic!("target_start > target_end");
+  }
+
+  Ok(
+    source[source_start..source_end].cmp(&target[target_start..target_end])
+      as i32,
+  )
+}
+
 #[op2]
 pub fn op_node_decode_utf8<'a>(
   scope: &mut v8::PinScope<'a, '_>,
@@ -170,6 +213,10 @@ enum BufferError {
   #[class(range)]
   #[property("code" = "ERR_OUT_OF_RANGE")]
   OutOfRange,
+  #[error("The value of \"{0}\" is out of range.")]
+  #[class(range)]
+  #[property("code" = "ERR_OUT_OF_RANGE")]
+  OutOfRangeNamed(String),
 }
 
 #[inline(always)]

filename: ext/node/polyfills/internal/buffer.mjs
patch: @@ -62,6 +62,8 @@ const {
 import {
   op_is_ascii,
   op_is_utf8,
+  op_node_buffer_compare,
+  op_node_buffer_compare_offset,
   op_node_call_is_from_dependency,
   op_node_decode_utf8,
   op_transcode,
@@ -534,22 +536,7 @@ const BufferCompare = Buffer.compare = function compare(a, b) {
   if (a === b) {
     return 0;
   }
-  let x = a.length;
-  let y = b.length;
-  for (let i = 0, len = MathMin(x, y); i < len; ++i) {
-    if (a[i] !== b[i]) {
-      x = a[i];
-      y = b[i];
-      break;
-    }
-  }
-  if (x < y) {
-    return -1;
-  }
-  if (y < x) {
-    return 1;
-  }
-  return 0;
+  return op_node_buffer_compare(a, b);
 };
 
 const BufferIsEncoding = Buffer.isEncoding = function isEncoding(encoding) {
@@ -819,14 +806,7 @@ Buffer.prototype.compare = function compare(
   thisStart,
   thisEnd,
 ) {
-  if (isUint8Array(target)) {
-    target = BufferFrom(
-      target,
-      TypedArrayPrototypeGetByteOffset(target),
-      TypedArrayPrototypeGetByteLength(target),
-    );
-  }
-  if (!BufferIsBuffer(target)) {
+  if (!isUint8Array(target)) {
     throw new codes.ERR_INVALID_ARG_TYPE(
       "target",
       ["Buffer", "Uint8Array"],
@@ -874,33 +854,14 @@ Buffer.prototype.compare = function compare(
   if (start >= end) {
     return 1;
   }
-  start >>>= 0;
-  end >>>= 0;
-  thisStart >>>= 0;
-  thisEnd >>>= 0;
-  if (this === target) {
-    return 0;
-  }
-  let x = thisEnd - thisStart;
-  let y = end - start;
-  const len = MathMin(x, y);
-  const thisCopy = TypedArrayPrototypeSlice(this, thisStart, thisEnd);
-  // deno-lint-ignore prefer-primordials
-  const targetCopy = target.slice(start, end);
-  for (let i = 0; i < len; ++i) {
-    if (thisCopy[i] !== targetCopy[i]) {
-      x = thisCopy[i];
-      y = targetCopy[i];
-      break;
-    }
-  }
-  if (x < y) {
-    return -1;
-  }
-  if (y < x) {
-    return 1;
-  }
-  return 0;
+  return op_node_buffer_compare_offset(
+    this,
+    target,
+    thisStart,
+    start,
+    thisEnd,
+    end,
+  );
 };
 
 function bidirectionalIndexOf(buffer, val, byteOffset, encoding, dir) {