buffer: improve performance of multiple Buffer operations

- copyBytesFrom: calculate byte offsets directly instead of
  slicing into an intermediate typed array
- toString('hex'): use V8 Uint8Array.prototype.toHex() builtin
- fill: add single-char ASCII fast path
- indexOf: use indexOfString directly for ASCII encoding
- swap16/32/64: add V8 Fast API functions

PR-URL: https://github.com/nodejs/node/pull/61871
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Ali Hassan authored and GitHub committed 2026-03-27 21:50:58 +00:00
1 parent 7d1f1b4483
commit 9e0dc8b472
10 files changed
+280 -70

No files matched your search

@@ -4,7 +4,7 @@ const common = require('../common');
const bench = common.createBenchmark(main, {
type: ['one_byte', 'two_bytes', 'three_bytes',
'four_bytes', 'latin1'],
encoding: ['utf8', 'base64'],
encoding: ['utf8', 'base64', 'latin1', 'hex'],
repeat: [1, 2, 16, 256], // x16
n: [4e6],
});
@@ -0,0 +1,31 @@
'use strict';
const common = require('../common.js');
const bench = common.createBenchmark(main, {
type: ['Uint8Array', 'Uint16Array', 'Uint32Array', 'Float64Array'],
len: [64, 256, 2048],
partial: ['none', 'offset', 'offset-length'],
n: [6e5],
});
function main({ n, len, type, partial }) {
const TypedArrayCtor = globalThis[type];
const src = new TypedArrayCtor(len);
for (let i = 0; i < len; i++) src[i] = i;
let offset;
let length;
if (partial === 'offset') {
offset = len >>> 2;
} else if (partial === 'offset-length') {
offset = len >>> 2;
length = len >>> 1;
}
bench.start();
for (let i = 0; i < n; i++) {
Buffer.copyBytesFrom(src, offset, length);
}
bench.end(n);
}
+1
View File
@@ -10,6 +10,7 @@ const bench = common.createBenchmark(main, {
'fill("t")',
'fill("test")',
'fill("t", "utf8")',
'fill("t", "ascii")',
'fill("t", 0, "utf8")',
'fill("t", 0)',
'fill(Buffer.alloc(1), 0)',
+1 -1
View File
@@ -19,7 +19,7 @@ const searchStrings = [
const bench = common.createBenchmark(main, {
search: searchStrings,
encoding: ['undefined', 'utf8', 'ucs2', 'latin1'],
encoding: ['undefined', 'utf8', 'ascii', 'latin1', 'ucs2'],
type: ['buffer', 'string'],
n: [5e4],
}, {
+1 -1
View File
@@ -3,7 +3,7 @@
const common = require('../common.js');
const bench = common.createBenchmark(main, {
encoding: ['', 'utf8', 'ascii', 'latin1', 'hex', 'UCS-2'],
encoding: ['', 'utf8', 'ascii', 'latin1', 'hex', 'base64', 'base64url', 'UCS-2'],
args: [0, 1, 3],
len: [1, 64, 1024],
n: [1e6],
+67 -54
View File
@@ -50,7 +50,6 @@ const {
TypedArrayPrototypeGetByteOffset,
TypedArrayPrototypeGetLength,
TypedArrayPrototypeSet,
TypedArrayPrototypeSlice,
TypedArrayPrototypeSubarray,
Uint8Array,
} = primordials;
@@ -383,28 +382,33 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) {
return new FastBuffer();
}
if (offset !== undefined || length !== undefined) {
if (offset !== undefined) {
validateInteger(offset, 'offset', 0);
if (offset >= viewLength) return new FastBuffer();
} else {
offset = 0;
}
let end;
if (length !== undefined) {
validateInteger(length, 'length', 0);
end = offset + length;
} else {
end = viewLength;
}
let start = 0;
let end = viewLength;
view = TypedArrayPrototypeSlice(view, offset, end);
if (offset !== undefined) {
validateInteger(offset, 'offset', 0);
if (offset >= viewLength) return new FastBuffer();
start = offset;
}
if (length !== undefined) {
validateInteger(length, 'length', 0);
// The old code used TypedArrayPrototypeSlice which clamps internally.
end = MathMin(start + length, viewLength);
}
if (end <= start) return new FastBuffer();
const viewByteLength = TypedArrayPrototypeGetByteLength(view);
const elementSize = viewByteLength / viewLength;
const srcByteOffset = TypedArrayPrototypeGetByteOffset(view) +
start * elementSize;
const srcByteLength = (end - start) * elementSize;
return fromArrayLike(new Uint8Array(
TypedArrayPrototypeGetBuffer(view),
TypedArrayPrototypeGetByteOffset(view),
TypedArrayPrototypeGetByteLength(view)));
srcByteOffset,
srcByteLength));
};
// Identical to the built-in %TypedArray%.of(), but avoids using the deprecated
@@ -551,14 +555,15 @@ function fromArrayBuffer(obj, byteOffset, length) {
}
function fromArrayLike(obj) {
if (obj.length <= 0)
const { length } = obj;
if (length <= 0)
return new FastBuffer();
if (obj.length < (Buffer.poolSize >>> 1)) {
if (obj.length > (poolSize - poolOffset))
if (length < (Buffer.poolSize >>> 1)) {
if (length > (poolSize - poolOffset))
createPool();
const b = new FastBuffer(allocPool, poolOffset, obj.length);
const b = new FastBuffer(allocPool, poolOffset, length);
TypedArrayPrototypeSet(b, obj, 0);
poolOffset += obj.length;
poolOffset += length;
alignPool();
return b;
}
@@ -732,11 +737,7 @@ const encodingOps = {
write: asciiWrite,
slice: asciiSlice,
indexOf: (buf, val, byteOffset, dir) =>
indexOfBuffer(buf,
fromStringFast(val, encodingOps.ascii),
byteOffset,
encodingsMap.ascii,
dir),
indexOfString(buf, val, byteOffset, encodingsMap.ascii, dir),
},
base64: {
encoding: 'base64',
@@ -897,17 +898,17 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
return utf8Slice(this, 0, this.length);
}
const len = this.length;
const bufferLength = TypedArrayPrototypeGetLength(this);
if (start <= 0)
start = 0;
else if (start >= len)
else if (start >= bufferLength)
return '';
else
start = MathTrunc(start) || 0;
if (end === undefined || end > len)
end = len;
if (end === undefined || end > bufferLength)
end = bufferLength;
else
end = MathTrunc(end) || 0;
@@ -1118,7 +1119,9 @@ function _fill(buf, value, offset, end, encoding) {
value = 0;
} else if (value.length === 1) {
// Fast path: If `value` fits into a single byte, use that numeric value.
if (normalizedEncoding === 'utf8') {
// ASCII shares this branch with utf8 since code < 128 covers the full
// ASCII range; anything outside falls through to C++ bindingFill.
if (normalizedEncoding === 'utf8' || normalizedEncoding === 'ascii') {
const code = StringPrototypeCharCodeAt(value, 0);
if (code < 128) {
value = code;
@@ -1168,21 +1171,22 @@ function _fill(buf, value, offset, end, encoding) {
}
Buffer.prototype.write = function write(string, offset, length, encoding) {
const bufferLength = TypedArrayPrototypeGetLength(this);
// Buffer#write(string);
if (offset === undefined) {
return utf8Write(this, string, 0, this.length);
return utf8Write(this, string, 0, bufferLength);
}
// Buffer#write(string, encoding)
if (length === undefined && typeof offset === 'string') {
encoding = offset;
length = this.length;
length = bufferLength;
offset = 0;
// Buffer#write(string, offset[, length][, encoding])
} else {
validateOffset(offset, 'offset', 0, this.length);
validateOffset(offset, 'offset', 0, bufferLength);
const remaining = this.length - offset;
const remaining = bufferLength - offset;
if (length === undefined) {
length = remaining;
@@ -1190,7 +1194,7 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
encoding = length;
length = remaining;
} else {
validateOffset(length, 'length', 0, this.length);
validateOffset(length, 'length', 0, bufferLength);
if (length > remaining)
length = remaining;
}
@@ -1208,9 +1212,10 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
};
Buffer.prototype.toJSON = function toJSON() {
if (this.length > 0) {
const data = new Array(this.length);
for (let i = 0; i < this.length; ++i)
const bufferLength = TypedArrayPrototypeGetLength(this);
if (bufferLength > 0) {
const data = new Array(bufferLength);
for (let i = 0; i < bufferLength; ++i)
data[i] = this[i];
return { type: 'Buffer', data };
}
@@ -1235,7 +1240,7 @@ function adjustOffset(offset, length) {
}
Buffer.prototype.subarray = function subarray(start, end) {
const srcLength = this.length;
const srcLength = TypedArrayPrototypeGetLength(this);
start = adjustOffset(start, srcLength);
end = end !== undefined ? adjustOffset(end, srcLength) : srcLength;
const newLength = end > start ? end - start : 0;
@@ -1253,45 +1258,52 @@ function swap(b, n, m) {
}
Buffer.prototype.swap16 = function swap16() {
// For Buffer.length < 128, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length <= 32, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
const len = TypedArrayPrototypeGetLength(this);
if (len % 2 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('16-bits');
if (len < 128) {
if (len <= 32) {
for (let i = 0; i < len; i += 2)
swap(this, i, i + 1);
return this;
}
return _swap16(this);
_swap16(this);
return this;
};
Buffer.prototype.swap32 = function swap32() {
// For Buffer.length < 192, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length <= 32, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
const len = TypedArrayPrototypeGetLength(this);
if (len % 4 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('32-bits');
if (len < 192) {
if (len <= 32) {
for (let i = 0; i < len; i += 4) {
swap(this, i, i + 3);
swap(this, i + 1, i + 2);
}
return this;
}
return _swap32(this);
_swap32(this);
return this;
};
Buffer.prototype.swap64 = function swap64() {
// For Buffer.length < 192, it's generally faster to
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
// For Buffer.length < 48, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
// Threshold differs from swap16/swap32 (<=32) because swap64's
// crossover is between 40 and 48 (native wins at 48, loses at 40).
const len = TypedArrayPrototypeGetLength(this);
if (len % 8 !== 0)
throw new ERR_INVALID_BUFFER_SIZE('64-bits');
if (len < 192) {
if (len < 48) {
for (let i = 0; i < len; i += 8) {
swap(this, i, i + 7);
swap(this, i + 1, i + 6);
@@ -1300,7 +1312,8 @@ Buffer.prototype.swap64 = function swap64() {
}
return this;
}
return _swap64(this);
_swap64(this);
return this;
};
Buffer.prototype.toLocaleString = Buffer.prototype.toString;
+44 -13
View File
@@ -1050,7 +1050,7 @@ void IndexOfString(const FunctionCallbackInfo<Value>& args) {
needle_length,
offset,
is_forward);
} else if (enc == LATIN1) {
} else if (enc == ASCII || enc == LATIN1) {
MaybeStackBuffer<uint8_t> needle_data(needle_length);
StringBytes::Write(isolate,
reinterpret_cast<char*>(needle_data.out()),
@@ -1196,31 +1196,59 @@ int32_t FastIndexOfNumber(Local<Value>,
static CFunction fast_index_of_number(CFunction::Make(FastIndexOfNumber));
void Swap16(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]);
DCHECK(args[0]->IsArrayBufferView());
SPREAD_BUFFER_ARG(args[0], ts_obj);
CHECK(nbytes::SwapBytes16(ts_obj_data, ts_obj_length));
args.GetReturnValue().Set(args[0]);
}
void FastSwap16(Local<Value> receiver,
Local<Value> buffer_obj,
// NOLINTNEXTLINE(runtime/references)
FastApiCallbackOptions& options) {
TRACK_V8_FAST_API_CALL("buffer.swap16");
HandleScope scope(options.isolate);
SPREAD_BUFFER_ARG(buffer_obj, ts_obj);
CHECK(nbytes::SwapBytes16(ts_obj_data, ts_obj_length));
}
static CFunction fast_swap16(CFunction::Make(FastSwap16));
void Swap32(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]);
DCHECK(args[0]->IsArrayBufferView());
SPREAD_BUFFER_ARG(args[0], ts_obj);
CHECK(nbytes::SwapBytes32(ts_obj_data, ts_obj_length));
args.GetReturnValue().Set(args[0]);
}
void FastSwap32(Local<Value> receiver,
Local<Value> buffer_obj,
// NOLINTNEXTLINE(runtime/references)
FastApiCallbackOptions& options) {
TRACK_V8_FAST_API_CALL("buffer.swap32");
HandleScope scope(options.isolate);
SPREAD_BUFFER_ARG(buffer_obj, ts_obj);
CHECK(nbytes::SwapBytes32(ts_obj_data, ts_obj_length));
}
static CFunction fast_swap32(CFunction::Make(FastSwap32));
void Swap64(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]);
DCHECK(args[0]->IsArrayBufferView());
SPREAD_BUFFER_ARG(args[0], ts_obj);
CHECK(nbytes::SwapBytes64(ts_obj_data, ts_obj_length));
args.GetReturnValue().Set(args[0]);
}
void FastSwap64(Local<Value> receiver,
Local<Value> buffer_obj,
// NOLINTNEXTLINE(runtime/references)
FastApiCallbackOptions& options) {
TRACK_V8_FAST_API_CALL("buffer.swap64");
HandleScope scope(options.isolate);
SPREAD_BUFFER_ARG(buffer_obj, ts_obj);
CHECK(nbytes::SwapBytes64(ts_obj_data, ts_obj_length));
}
static CFunction fast_swap64(CFunction::Make(FastSwap64));
static void IsUtf8(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 1);
@@ -1619,9 +1647,9 @@ void Initialize(Local<Object> target,
SetMethodNoSideEffect(
context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer);
SetMethod(context, target, "swap16", Swap16);
SetMethod(context, target, "swap32", Swap32);
SetMethod(context, target, "swap64", Swap64);
SetFastMethod(context, target, "swap16", Swap16, &fast_swap16);
SetFastMethod(context, target, "swap32", Swap32, &fast_swap32);
SetFastMethod(context, target, "swap64", Swap64, &fast_swap64);
SetMethodNoSideEffect(context, target, "isUtf8", IsUtf8);
SetMethodNoSideEffect(context, target, "isAscii", IsAscii);
@@ -1690,8 +1718,11 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IndexOfString);
registry->Register(Swap16);
registry->Register(fast_swap16);
registry->Register(Swap32);
registry->Register(fast_swap32);
registry->Register(Swap64);
registry->Register(fast_swap64);
registry->Register(IsUtf8);
registry->Register(IsAscii);
+39
View File
@@ -140,5 +140,44 @@ assert.throws(() => {
})
);
// copyBytesFrom: length exceeds view (should clamp, not throw)
{
const u8 = new Uint8Array([1, 2, 3, 4, 5]);
const b = Buffer.copyBytesFrom(u8, 2, 100);
assert.strictEqual(b.length, 3);
assert.deepStrictEqual([...b], [3, 4, 5]);
}
// copyBytesFrom: length 0 returns empty buffer
{
const u8 = new Uint8Array([1, 2, 3]);
const b = Buffer.copyBytesFrom(u8, 1, 0);
assert.strictEqual(b.length, 0);
}
// copyBytesFrom: offset past end returns empty buffer
{
const u8 = new Uint8Array([1, 2, 3]);
const b = Buffer.copyBytesFrom(u8, 10);
assert.strictEqual(b.length, 0);
}
// copyBytesFrom: Float64Array with offset and length
{
const f64 = new Float64Array([1.0, 2.0, 3.0, 4.0]);
const b = Buffer.copyBytesFrom(f64, 1, 2);
assert.strictEqual(b.length, 16);
const view = new Float64Array(b.buffer, b.byteOffset, 2);
assert.strictEqual(view[0], 2.0);
assert.strictEqual(view[1], 3.0);
}
// copyBytesFrom: empty typed array returns empty buffer
{
const empty = new Uint8Array(0);
const b = Buffer.copyBytesFrom(empty);
assert.strictEqual(b.length, 0);
}
// Invalid encoding is allowed
Buffer.from('asd', 1);
+55
View File
@@ -0,0 +1,55 @@
// Flags: --expose-internals --no-warnings --allow-natives-syntax
'use strict';
const common = require('../common');
const assert = require('assert');
function testFastSwap16() {
const buf = Buffer.from([0x01, 0x02, 0x03, 0x04]);
const expected = Buffer.from([0x02, 0x01, 0x04, 0x03]);
const padded = Buffer.alloc(256);
buf.copy(padded);
padded.swap16();
assert.deepStrictEqual(padded.subarray(0, 4), expected);
}
function testFastSwap32() {
const buf = Buffer.from([0x01, 0x02, 0x03, 0x04]);
const expected = Buffer.from([0x04, 0x03, 0x02, 0x01]);
const padded = Buffer.alloc(256);
buf.copy(padded);
padded.swap32();
assert.deepStrictEqual(padded.subarray(0, 4), expected);
}
function testFastSwap64() {
const buf = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
const expected = Buffer.from([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]);
const padded = Buffer.alloc(256);
buf.copy(padded);
padded.swap64();
assert.deepStrictEqual(padded.subarray(0, 8), expected);
}
eval('%PrepareFunctionForOptimization(Buffer.prototype.swap16)');
testFastSwap16();
eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap16)');
testFastSwap16();
eval('%PrepareFunctionForOptimization(Buffer.prototype.swap32)');
testFastSwap32();
eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap32)');
testFastSwap32();
eval('%PrepareFunctionForOptimization(Buffer.prototype.swap64)');
testFastSwap64();
eval('%OptimizeFunctionOnNextCall(Buffer.prototype.swap64)');
testFastSwap64();
if (common.isDebug) {
const { internalBinding } = require('internal/test/binding');
const { getV8FastApiCallCount } = internalBinding('debug');
assert.strictEqual(getV8FastApiCallCount('buffer.swap16'), 1);
assert.strictEqual(getV8FastApiCallCount('buffer.swap32'), 1);
assert.strictEqual(getV8FastApiCallCount('buffer.swap64'), 1);
}
@@ -78,6 +78,46 @@ assert.strictEqual(rangeBuffer.toString('ascii', 0, '-1.99'), '');
assert.strictEqual(rangeBuffer.toString('ascii', 0, 1.99), 'a');
assert.strictEqual(rangeBuffer.toString('ascii', 0, true), 'a');
// Test hex/base64/base64url partial range encoding (exercises V8 builtin path)
{
const buf = Buffer.from([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]);
assert.strictEqual(buf.toString('hex'), 'deadbeefcafe');
assert.strictEqual(buf.toString('hex', 0, 3), 'deadbe');
assert.strictEqual(buf.toString('hex', 2, 5), 'beefca');
assert.strictEqual(buf.toString('hex', 4), 'cafe');
assert.strictEqual(buf.toString('hex', 6), '');
assert.strictEqual(buf.toString('hex', 0, 0), '');
}
{
const buf = Buffer.from('Hello, World!');
assert.strictEqual(buf.toString('base64'), 'SGVsbG8sIFdvcmxkIQ==');
assert.strictEqual(buf.toString('base64', 0, 5), 'SGVsbG8=');
assert.strictEqual(buf.toString('base64', 7), 'V29ybGQh');
assert.strictEqual(buf.toString('base64', 0, 0), '');
}
{
const buf = Buffer.from('Hello, World!');
assert.strictEqual(buf.toString('base64url'), 'SGVsbG8sIFdvcmxkIQ');
assert.strictEqual(buf.toString('base64url', 0, 5), 'SGVsbG8');
assert.strictEqual(buf.toString('base64url', 7), 'V29ybGQh');
assert.strictEqual(buf.toString('base64url', 0, 0), '');
}
// Test with pool-allocated buffer (has non-zero byteOffset)
{
const poolBuf = Buffer.from('test data for hex encoding');
assert.strictEqual(
poolBuf.toString('hex'),
Buffer.from('test data for hex encoding').toString('hex')
);
assert.strictEqual(
poolBuf.toString('hex', 5, 9),
Buffer.from('data').toString('hex')
);
}
// Try toString() with an object as an encoding
assert.strictEqual(rangeBuffer.toString({ toString: function() {
return 'ascii';