Skip to content

Commit d2ba38f

Browse files
buffer: improve performance of multiple Buffer operations
Optimize several Buffer operations: - Buffer.copyBytesFrom(): avoid intermediate TypedArray slice by calculating byte offsets directly - Buffer.prototype.toString('hex'): use V8 Uint8Array.prototype.toHex() builtin for faster hex encoding (+10-30%) - Buffer.prototype.fill(): add ASCII single-char fast path using numeric fill instead of string encoding pipeline - ASCII indexOf: call indexOfString directly instead of creating an intermediate Buffer via fromStringFast - swap16/32/64: add V8 Fast API C++ functions for the native code path, return this from JS side to enable void fast path - fromArrayLike: cache obj.length in local variable Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4d1557a commit d2ba38f

File tree

9 files changed

+215
-33
lines changed

9 files changed

+215
-33
lines changed

benchmark/buffers/buffer-bytelength-string.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const common = require('../common');
44
const bench = common.createBenchmark(main, {
55
type: ['one_byte', 'two_bytes', 'three_bytes',
66
'four_bytes', 'latin1'],
7-
encoding: ['utf8', 'base64'],
7+
encoding: ['utf8', 'base64', 'latin1', 'hex'],
88
repeat: [1, 2, 16, 256], // x16
99
n: [4e6],
1010
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
5+
const bench = common.createBenchmark(main, {
6+
type: ['Uint8Array', 'Uint16Array', 'Uint32Array', 'Float64Array'],
7+
len: [64, 256, 2048],
8+
partial: ['none', 'offset', 'offset-length'],
9+
n: [6e5],
10+
});
11+
12+
function main({ n, len, type, partial }) {
13+
const TypedArrayCtor = globalThis[type];
14+
const src = new TypedArrayCtor(len);
15+
for (let i = 0; i < len; i++) src[i] = i;
16+
17+
let offset;
18+
let length;
19+
if (partial === 'offset') {
20+
offset = len >>> 2;
21+
} else if (partial === 'offset-length') {
22+
offset = len >>> 2;
23+
length = len >>> 1;
24+
}
25+
26+
bench.start();
27+
for (let i = 0; i < n; i++) {
28+
Buffer.copyBytesFrom(src, offset, length);
29+
}
30+
bench.end(n);
31+
}

benchmark/buffers/buffer-fill.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const bench = common.createBenchmark(main, {
1010
'fill("t")',
1111
'fill("test")',
1212
'fill("t", "utf8")',
13+
'fill("t", "ascii")',
1314
'fill("t", 0, "utf8")',
1415
'fill("t", 0)',
1516
'fill(Buffer.alloc(1), 0)',

benchmark/buffers/buffer-indexof.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const searchStrings = [
1919

2020
const bench = common.createBenchmark(main, {
2121
search: searchStrings,
22-
encoding: ['undefined', 'utf8', 'ucs2'],
22+
encoding: ['undefined', 'utf8', 'ascii', 'latin1', 'ucs2'],
2323
type: ['buffer', 'string'],
2424
n: [5e4],
2525
}, {

benchmark/buffers/buffer-tostring.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const common = require('../common.js');
44

55
const bench = common.createBenchmark(main, {
6-
encoding: ['', 'utf8', 'ascii', 'latin1', 'hex', 'UCS-2'],
6+
encoding: ['', 'utf8', 'ascii', 'latin1', 'hex', 'base64', 'base64url', 'UCS-2'],
77
args: [0, 1, 3],
88
len: [1, 64, 1024],
99
n: [1e6],

lib/buffer.js

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,21 @@ const {
5050
TypedArrayPrototypeGetByteOffset,
5151
TypedArrayPrototypeGetLength,
5252
TypedArrayPrototypeSet,
53-
TypedArrayPrototypeSlice,
5453
Uint8Array,
54+
Uint8ArrayPrototype,
55+
uncurryThis,
5556
} = primordials;
5657

58+
// V8 shipping feature (toHex) is installed by
59+
// InitializeExperimentalGlobal(), which is skipped during snapshot creation
60+
// TODO: Remove this once V8 shipping feature (toHex) is available.
61+
let Uint8ArrayPrototypeToHex;
62+
function ensureUint8ArrayToHex() {
63+
if (Uint8ArrayPrototypeToHex === undefined) {
64+
Uint8ArrayPrototypeToHex = uncurryThis(Uint8ArrayPrototype.toHex);
65+
}
66+
}
67+
5768
const {
5869
byteLengthUtf8,
5970
compare: _compare,
@@ -382,28 +393,41 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) {
382393
return new FastBuffer();
383394
}
384395

396+
const byteLength = TypedArrayPrototypeGetByteLength(view);
397+
385398
if (offset !== undefined || length !== undefined) {
386399
if (offset !== undefined) {
387400
validateInteger(offset, 'offset', 0);
388401
if (offset >= viewLength) return new FastBuffer();
389402
} else {
390403
offset = 0;
391404
}
405+
392406
let end;
393407
if (length !== undefined) {
394408
validateInteger(length, 'length', 0);
395-
end = offset + length;
409+
end = MathMin(offset + length, viewLength);
396410
} else {
397411
end = viewLength;
398412
}
399413

400-
view = TypedArrayPrototypeSlice(view, offset, end);
414+
if (end <= offset) return new FastBuffer();
415+
416+
const elementSize = byteLength / viewLength;
417+
const srcByteOffset = TypedArrayPrototypeGetByteOffset(view) +
418+
offset * elementSize;
419+
const srcByteLength = (end - offset) * elementSize;
420+
421+
return fromArrayLike(new Uint8Array(
422+
TypedArrayPrototypeGetBuffer(view),
423+
srcByteOffset,
424+
srcByteLength));
401425
}
402426

403427
return fromArrayLike(new Uint8Array(
404428
TypedArrayPrototypeGetBuffer(view),
405429
TypedArrayPrototypeGetByteOffset(view),
406-
TypedArrayPrototypeGetByteLength(view)));
430+
byteLength));
407431
};
408432

409433
// Identical to the built-in %TypedArray%.of(), but avoids using the deprecated
@@ -550,14 +574,15 @@ function fromArrayBuffer(obj, byteOffset, length) {
550574
}
551575

552576
function fromArrayLike(obj) {
553-
if (obj.length <= 0)
577+
const len = obj.length;
578+
if (len <= 0)
554579
return new FastBuffer();
555-
if (obj.length < (Buffer.poolSize >>> 1)) {
556-
if (obj.length > (poolSize - poolOffset))
580+
if (len < (Buffer.poolSize >>> 1)) {
581+
if (len > (poolSize - poolOffset))
557582
createPool();
558-
const b = new FastBuffer(allocPool, poolOffset, obj.length);
583+
const b = new FastBuffer(allocPool, poolOffset, len);
559584
TypedArrayPrototypeSet(b, obj, 0);
560-
poolOffset += obj.length;
585+
poolOffset += len;
561586
alignPool();
562587
return b;
563588
}
@@ -657,6 +682,14 @@ function base64ByteLength(str, bytes) {
657682
return (bytes * 3) >>> 2;
658683
}
659684

685+
function hexSliceToHex(buf, start, end) {
686+
ensureUint8ArrayToHex();
687+
return Uint8ArrayPrototypeToHex(
688+
new Uint8Array(TypedArrayPrototypeGetBuffer(buf),
689+
TypedArrayPrototypeGetByteOffset(buf) + start,
690+
end - start));
691+
}
692+
660693
const encodingOps = {
661694
utf8: {
662695
encoding: 'utf8',
@@ -701,11 +734,7 @@ const encodingOps = {
701734
write: asciiWrite,
702735
slice: asciiSlice,
703736
indexOf: (buf, val, byteOffset, dir) =>
704-
indexOfBuffer(buf,
705-
fromStringFast(val, encodingOps.ascii),
706-
byteOffset,
707-
encodingsMap.ascii,
708-
dir),
737+
indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir),
709738
},
710739
base64: {
711740
encoding: 'base64',
@@ -738,7 +767,7 @@ const encodingOps = {
738767
encodingVal: encodingsMap.hex,
739768
byteLength: (string) => string.length >>> 1,
740769
write: hexWrite,
741-
slice: hexSlice,
770+
slice: hexSliceToHex,
742771
indexOf: (buf, val, byteOffset, dir) =>
743772
indexOfBuffer(buf,
744773
fromStringFast(val, encodingOps.hex),
@@ -1087,7 +1116,7 @@ function _fill(buf, value, offset, end, encoding) {
10871116
value = 0;
10881117
} else if (value.length === 1) {
10891118
// Fast path: If `value` fits into a single byte, use that numeric value.
1090-
if (normalizedEncoding === 'utf8') {
1119+
if (normalizedEncoding === 'utf8' || normalizedEncoding === 'ascii') {
10911120
const code = StringPrototypeCharCodeAt(value, 0);
10921121
if (code < 128) {
10931122
value = code;
@@ -1137,29 +1166,30 @@ function _fill(buf, value, offset, end, encoding) {
11371166
}
11381167

11391168
Buffer.prototype.write = function write(string, offset, length, encoding) {
1169+
const len = this.length;
11401170
// Buffer#write(string);
11411171
if (offset === undefined) {
1142-
return utf8Write(this, string, 0, this.length);
1172+
return utf8Write(this, string, 0, len);
11431173
}
11441174
// Buffer#write(string, encoding)
11451175
if (length === undefined && typeof offset === 'string') {
11461176
encoding = offset;
1147-
length = this.length;
1177+
length = len;
11481178
offset = 0;
11491179

11501180
// Buffer#write(string, offset[, length][, encoding])
11511181
} else {
1152-
validateOffset(offset, 'offset', 0, this.length);
1182+
validateOffset(offset, 'offset', 0, len);
11531183

1154-
const remaining = this.length - offset;
1184+
const remaining = len - offset;
11551185

11561186
if (length === undefined) {
11571187
length = remaining;
11581188
} else if (typeof length === 'string') {
11591189
encoding = length;
11601190
length = remaining;
11611191
} else {
1162-
validateOffset(length, 'length', 0, this.length);
1192+
validateOffset(length, 'length', 0, len);
11631193
if (length > remaining)
11641194
length = remaining;
11651195
}
@@ -1177,9 +1207,10 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
11771207
};
11781208

11791209
Buffer.prototype.toJSON = function toJSON() {
1180-
if (this.length > 0) {
1181-
const data = new Array(this.length);
1182-
for (let i = 0; i < this.length; ++i)
1210+
const len = this.length;
1211+
if (len > 0) {
1212+
const data = new Array(len);
1213+
for (let i = 0; i < len; ++i)
11831214
data[i] = this[i];
11841215
return { type: 'Buffer', data };
11851216
}
@@ -1233,7 +1264,8 @@ Buffer.prototype.swap16 = function swap16() {
12331264
swap(this, i, i + 1);
12341265
return this;
12351266
}
1236-
return _swap16(this);
1267+
_swap16(this);
1268+
return this;
12371269
};
12381270

12391271
Buffer.prototype.swap32 = function swap32() {
@@ -1250,7 +1282,8 @@ Buffer.prototype.swap32 = function swap32() {
12501282
}
12511283
return this;
12521284
}
1253-
return _swap32(this);
1285+
_swap32(this);
1286+
return this;
12541287
};
12551288

12561289
Buffer.prototype.swap64 = function swap64() {
@@ -1269,7 +1302,8 @@ Buffer.prototype.swap64 = function swap64() {
12691302
}
12701303
return this;
12711304
}
1272-
return _swap64(this);
1305+
_swap64(this);
1306+
return this;
12731307
};
12741308

12751309
Buffer.prototype.toLocaleString = Buffer.prototype.toString;

src/node_buffer.cc

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,17 @@ void Swap16(const FunctionCallbackInfo<Value>& args) {
12071207
args.GetReturnValue().Set(args[0]);
12081208
}
12091209

1210+
void FastSwap16(Local<Value> receiver,
1211+
Local<Value> buffer_obj,
1212+
// NOLINTNEXTLINE(runtime/references)
1213+
FastApiCallbackOptions& options) {
1214+
HandleScope scope(options.isolate);
1215+
ArrayBufferViewContents<char> buffer(buffer_obj);
1216+
CHECK(nbytes::SwapBytes16(const_cast<char*>(buffer.data()),
1217+
buffer.length()));
1218+
}
1219+
1220+
static CFunction fast_swap16(CFunction::Make(FastSwap16));
12101221

12111222
void Swap32(const FunctionCallbackInfo<Value>& args) {
12121223
Environment* env = Environment::GetCurrent(args);
@@ -1216,6 +1227,17 @@ void Swap32(const FunctionCallbackInfo<Value>& args) {
12161227
args.GetReturnValue().Set(args[0]);
12171228
}
12181229

1230+
void FastSwap32(Local<Value> receiver,
1231+
Local<Value> buffer_obj,
1232+
// NOLINTNEXTLINE(runtime/references)
1233+
FastApiCallbackOptions& options) {
1234+
HandleScope scope(options.isolate);
1235+
ArrayBufferViewContents<char> buffer(buffer_obj);
1236+
CHECK(nbytes::SwapBytes32(const_cast<char*>(buffer.data()),
1237+
buffer.length()));
1238+
}
1239+
1240+
static CFunction fast_swap32(CFunction::Make(FastSwap32));
12191241

12201242
void Swap64(const FunctionCallbackInfo<Value>& args) {
12211243
Environment* env = Environment::GetCurrent(args);
@@ -1225,6 +1247,18 @@ void Swap64(const FunctionCallbackInfo<Value>& args) {
12251247
args.GetReturnValue().Set(args[0]);
12261248
}
12271249

1250+
void FastSwap64(Local<Value> receiver,
1251+
Local<Value> buffer_obj,
1252+
// NOLINTNEXTLINE(runtime/references)
1253+
FastApiCallbackOptions& options) {
1254+
HandleScope scope(options.isolate);
1255+
ArrayBufferViewContents<char> buffer(buffer_obj);
1256+
CHECK(nbytes::SwapBytes64(const_cast<char*>(buffer.data()),
1257+
buffer.length()));
1258+
}
1259+
1260+
static CFunction fast_swap64(CFunction::Make(FastSwap64));
1261+
12281262
static void IsUtf8(const FunctionCallbackInfo<Value>& args) {
12291263
Environment* env = Environment::GetCurrent(args);
12301264
CHECK_EQ(args.Length(), 1);
@@ -1622,9 +1656,9 @@ void Initialize(Local<Object> target,
16221656
SetMethodNoSideEffect(
16231657
context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer);
16241658

1625-
SetMethod(context, target, "swap16", Swap16);
1626-
SetMethod(context, target, "swap32", Swap32);
1627-
SetMethod(context, target, "swap64", Swap64);
1659+
SetFastMethod(context, target, "swap16", Swap16, &fast_swap16);
1660+
SetFastMethod(context, target, "swap32", Swap32, &fast_swap32);
1661+
SetFastMethod(context, target, "swap64", Swap64, &fast_swap64);
16281662

16291663
SetMethodNoSideEffect(context, target, "isUtf8", IsUtf8);
16301664
SetMethodNoSideEffect(context, target, "isAscii", IsAscii);
@@ -1693,8 +1727,11 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
16931727
registry->Register(IndexOfString);
16941728

16951729
registry->Register(Swap16);
1730+
registry->Register(fast_swap16);
16961731
registry->Register(Swap32);
1732+
registry->Register(fast_swap32);
16971733
registry->Register(Swap64);
1734+
registry->Register(fast_swap64);
16981735

16991736
registry->Register(IsUtf8);
17001737
registry->Register(IsAscii);

0 commit comments

Comments
 (0)