From f01467d3dc395d6ef15f15e5269caba78d8def12 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Fri, 16 Jan 2026 05:10:47 +0900 Subject: [PATCH] perf(buffer): optimize Buffer.from(array) by using setFromArrayLike directly (#26135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Optimizes `Buffer.from(array)` by bypassing `JSC::construct()` overhead (~30ns) and leveraging JSC's internal array optimizations. ## Changes - For JSArray inputs, directly use `setFromArrayLike()` which internally detects array indexing types (Int32Shape/DoubleShape) and uses bulk copy operations (`copyFromInt32ShapeArray`/`copyFromDoubleShapeArray`) - Array-like objects and iterables continue to use the existing slow path - Added mitata benchmark for measuring performance ## Benchmark Results | Test | Before | After | Improvement | |------|--------|-------|-------------| | Buffer.from(int32[8]) | ~85ns | ~43ns | ~50% faster | | Buffer.from(int32[64]) | ~207ns | ~120ns | ~42% faster | | Buffer.from(int32[1024]) | ~1.85μs | ~1.32μs | ~29% faster | | Buffer.from(double[8]) | ~86ns | ~50ns | ~42% faster | | Buffer.from(double[64]) | ~212ns | ~151ns | ~29% faster | Bun is now faster than Node.js for these operations. ## Test All 449 buffer tests pass. --- bench/snippets/buffer-from-array.mjs | 38 ++++++++++++++++++++++++++++ src/bun.js/bindings/JSBuffer.cpp | 31 ++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 bench/snippets/buffer-from-array.mjs diff --git a/bench/snippets/buffer-from-array.mjs b/bench/snippets/buffer-from-array.mjs new file mode 100644 index 0000000000..7709ce3944 --- /dev/null +++ b/bench/snippets/buffer-from-array.mjs @@ -0,0 +1,38 @@ +// @runtime bun,node +import { Buffer } from "node:buffer"; +import { bench, group, run } from "../runner.mjs"; + +// Small arrays (common case) +const int32Array8 = [1, 2, 3, 4, 5, 6, 7, 8]; +const doubleArray8 = [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5]; + +// Medium arrays +const int32Array64 = Array.from({ length: 64 }, (_, i) => i % 256); +const doubleArray64 = Array.from({ length: 64 }, (_, i) => i + 0.5); + +// Large arrays +const int32Array1024 = Array.from({ length: 1024 }, (_, i) => i % 256); + +// Array-like objects (fallback path) +const arrayLike8 = { 0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, length: 8 }; + +// Empty array +const emptyArray = []; + +group("Buffer.from(array) - Int32 arrays", () => { + bench("Buffer.from(int32[8])", () => Buffer.from(int32Array8)); + bench("Buffer.from(int32[64])", () => Buffer.from(int32Array64)); + bench("Buffer.from(int32[1024])", () => Buffer.from(int32Array1024)); +}); + +group("Buffer.from(array) - Double arrays", () => { + bench("Buffer.from(double[8])", () => Buffer.from(doubleArray8)); + bench("Buffer.from(double[64])", () => Buffer.from(doubleArray64)); +}); + +group("Buffer.from(array) - Edge cases", () => { + bench("Buffer.from([])", () => Buffer.from(emptyArray)); + bench("Buffer.from(arrayLike[8])", () => Buffer.from(arrayLike8)); +}); + +await run(); diff --git a/src/bun.js/bindings/JSBuffer.cpp b/src/bun.js/bindings/JSBuffer.cpp index 867a689fba..fbc4887698 100644 --- a/src/bun.js/bindings/JSBuffer.cpp +++ b/src/bun.js/bindings/JSBuffer.cpp @@ -77,6 +77,8 @@ // #include #include +#include +#include extern "C" bool Bun__Node__ZeroFillBuffers; @@ -2859,11 +2861,38 @@ EncodedJSValue constructBufferFromArray(JSC::ThrowScope& throwScope, JSGlobalObj { auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + // FIXME: Further optimization possible by calling copyFromInt32ShapeArray/copyFromDoubleShapeArray. + if (JSArray* array = jsDynamicCast(arrayValue)) { + if (isJSArray(array)) { + size_t length = array->length(); + + // Empty array case + if (length == 0) + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(createEmptyBuffer(lexicalGlobalObject))); + + // Allocate uninitialized buffer + auto* uint8Array = createUninitializedBuffer(lexicalGlobalObject, length); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!uint8Array) [[unlikely]] { + throwOutOfMemoryError(lexicalGlobalObject, throwScope); + return {}; + } + + // setFromArrayLike internally detects Int32Shape/DoubleShape and uses + // copyFromInt32ShapeArray/copyFromDoubleShapeArray for bulk copy + bool success = uint8Array->setFromArrayLike(lexicalGlobalObject, 0, array, 0, length); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!success) + return {}; + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(uint8Array)); + } + } + + // Slow path: array-like objects, iterables auto* constructor = lexicalGlobalObject->m_typedArrayUint8.constructor(lexicalGlobalObject); MarkedArgumentBuffer argsBuffer; argsBuffer.append(arrayValue); JSValue target = globalObject->JSBufferConstructor(); - // TODO: I wish we could avoid this - it adds ~30ns of overhead just using JSC::construct. auto* object = JSC::construct(lexicalGlobalObject, constructor, target, argsBuffer, "Buffer failed to construct"_s); RETURN_IF_EXCEPTION(throwScope, {}); RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(object));