Skip to content

Commit e2f03c8

Browse files
thisalihassanaduh95
authored andcommitted
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: #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>
1 parent 2c63d30 commit e2f03c8

File tree

10 files changed

+280
-70
lines changed

10 files changed

+280
-70
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', 'latin1'],
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: 67 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ const {
5050
TypedArrayPrototypeGetByteOffset,
5151
TypedArrayPrototypeGetLength,
5252
TypedArrayPrototypeSet,
53-
TypedArrayPrototypeSlice,
5453
TypedArrayPrototypeSubarray,
5554
Uint8Array,
5655
} = primordials;
@@ -383,28 +382,33 @@ Buffer.copyBytesFrom = function copyBytesFrom(view, offset, length) {
383382
return new FastBuffer();
384383
}
385384

386-
if (offset !== undefined || length !== undefined) {
387-
if (offset !== undefined) {
388-
validateInteger(offset, 'offset', 0);
389-
if (offset >= viewLength) return new FastBuffer();
390-
} else {
391-
offset = 0;
392-
}
393-
let end;
394-
if (length !== undefined) {
395-
validateInteger(length, 'length', 0);
396-
end = offset + length;
397-
} else {
398-
end = viewLength;
399-
}
385+
let start = 0;
386+
let end = viewLength;
400387

401-
view = TypedArrayPrototypeSlice(view, offset, end);
388+
if (offset !== undefined) {
389+
validateInteger(offset, 'offset', 0);
390+
if (offset >= viewLength) return new FastBuffer();
391+
start = offset;
402392
}
403393

394+
if (length !== undefined) {
395+
validateInteger(length, 'length', 0);
396+
// The old code used TypedArrayPrototypeSlice which clamps internally.
397+
end = MathMin(start + length, viewLength);
398+
}
399+
400+
if (end <= start) return new FastBuffer();
401+
402+
const viewByteLength = TypedArrayPrototypeGetByteLength(view);
403+
const elementSize = viewByteLength / viewLength;
404+
const srcByteOffset = TypedArrayPrototypeGetByteOffset(view) +
405+
start * elementSize;
406+
const srcByteLength = (end - start) * elementSize;
407+
404408
return fromArrayLike(new Uint8Array(
405409
TypedArrayPrototypeGetBuffer(view),
406-
TypedArrayPrototypeGetByteOffset(view),
407-
TypedArrayPrototypeGetByteLength(view)));
410+
srcByteOffset,
411+
srcByteLength));
408412
};
409413

410414
// Identical to the built-in %TypedArray%.of(), but avoids using the deprecated
@@ -551,14 +555,15 @@ function fromArrayBuffer(obj, byteOffset, length) {
551555
}
552556

553557
function fromArrayLike(obj) {
554-
if (obj.length <= 0)
558+
const { length } = obj;
559+
if (length <= 0)
555560
return new FastBuffer();
556-
if (obj.length < (Buffer.poolSize >>> 1)) {
557-
if (obj.length > (poolSize - poolOffset))
561+
if (length < (Buffer.poolSize >>> 1)) {
562+
if (length > (poolSize - poolOffset))
558563
createPool();
559-
const b = new FastBuffer(allocPool, poolOffset, obj.length);
564+
const b = new FastBuffer(allocPool, poolOffset, length);
560565
TypedArrayPrototypeSet(b, obj, 0);
561-
poolOffset += obj.length;
566+
poolOffset += length;
562567
alignPool();
563568
return b;
564569
}
@@ -732,11 +737,7 @@ const encodingOps = {
732737
write: asciiWrite,
733738
slice: asciiSlice,
734739
indexOf: (buf, val, byteOffset, dir) =>
735-
indexOfBuffer(buf,
736-
fromStringFast(val, encodingOps.ascii),
737-
byteOffset,
738-
encodingsMap.ascii,
739-
dir),
740+
indexOfString(buf, val, byteOffset, encodingsMap.ascii, dir),
740741
},
741742
base64: {
742743
encoding: 'base64',
@@ -897,17 +898,17 @@ Buffer.prototype.toString = function toString(encoding, start, end) {
897898
return utf8Slice(this, 0, this.length);
898899
}
899900

900-
const len = this.length;
901+
const bufferLength = TypedArrayPrototypeGetLength(this);
901902

902903
if (start <= 0)
903904
start = 0;
904-
else if (start >= len)
905+
else if (start >= bufferLength)
905906
return '';
906907
else
907908
start = MathTrunc(start) || 0;
908909

909-
if (end === undefined || end > len)
910-
end = len;
910+
if (end === undefined || end > bufferLength)
911+
end = bufferLength;
911912
else
912913
end = MathTrunc(end) || 0;
913914

@@ -1118,7 +1119,9 @@ function _fill(buf, value, offset, end, encoding) {
11181119
value = 0;
11191120
} else if (value.length === 1) {
11201121
// Fast path: If `value` fits into a single byte, use that numeric value.
1121-
if (normalizedEncoding === 'utf8') {
1122+
// ASCII shares this branch with utf8 since code < 128 covers the full
1123+
// ASCII range; anything outside falls through to C++ bindingFill.
1124+
if (normalizedEncoding === 'utf8' || normalizedEncoding === 'ascii') {
11221125
const code = StringPrototypeCharCodeAt(value, 0);
11231126
if (code < 128) {
11241127
value = code;
@@ -1168,29 +1171,30 @@ function _fill(buf, value, offset, end, encoding) {
11681171
}
11691172

11701173
Buffer.prototype.write = function write(string, offset, length, encoding) {
1174+
const bufferLength = TypedArrayPrototypeGetLength(this);
11711175
// Buffer#write(string);
11721176
if (offset === undefined) {
1173-
return utf8Write(this, string, 0, this.length);
1177+
return utf8Write(this, string, 0, bufferLength);
11741178
}
11751179
// Buffer#write(string, encoding)
11761180
if (length === undefined && typeof offset === 'string') {
11771181
encoding = offset;
1178-
length = this.length;
1182+
length = bufferLength;
11791183
offset = 0;
11801184

11811185
// Buffer#write(string, offset[, length][, encoding])
11821186
} else {
1183-
validateOffset(offset, 'offset', 0, this.length);
1187+
validateOffset(offset, 'offset', 0, bufferLength);
11841188

1185-
const remaining = this.length - offset;
1189+
const remaining = bufferLength - offset;
11861190

11871191
if (length === undefined) {
11881192
length = remaining;
11891193
} else if (typeof length === 'string') {
11901194
encoding = length;
11911195
length = remaining;
11921196
} else {
1193-
validateOffset(length, 'length', 0, this.length);
1197+
validateOffset(length, 'length', 0, bufferLength);
11941198
if (length > remaining)
11951199
length = remaining;
11961200
}
@@ -1208,9 +1212,10 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
12081212
};
12091213

12101214
Buffer.prototype.toJSON = function toJSON() {
1211-
if (this.length > 0) {
1212-
const data = new Array(this.length);
1213-
for (let i = 0; i < this.length; ++i)
1215+
const bufferLength = TypedArrayPrototypeGetLength(this);
1216+
if (bufferLength > 0) {
1217+
const data = new Array(bufferLength);
1218+
for (let i = 0; i < bufferLength; ++i)
12141219
data[i] = this[i];
12151220
return { type: 'Buffer', data };
12161221
}
@@ -1235,7 +1240,7 @@ function adjustOffset(offset, length) {
12351240
}
12361241

12371242
Buffer.prototype.subarray = function subarray(start, end) {
1238-
const srcLength = this.length;
1243+
const srcLength = TypedArrayPrototypeGetLength(this);
12391244
start = adjustOffset(start, srcLength);
12401245
end = end !== undefined ? adjustOffset(end, srcLength) : srcLength;
12411246
const newLength = end > start ? end - start : 0;
@@ -1253,45 +1258,52 @@ function swap(b, n, m) {
12531258
}
12541259

12551260
Buffer.prototype.swap16 = function swap16() {
1256-
// For Buffer.length < 128, it's generally faster to
1261+
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
1262+
// For Buffer.length <= 32, it's generally faster to
12571263
// do the swap in javascript. For larger buffers,
12581264
// dropping down to the native code is faster.
1259-
const len = this.length;
1265+
const len = TypedArrayPrototypeGetLength(this);
12601266
if (len % 2 !== 0)
12611267
throw new ERR_INVALID_BUFFER_SIZE('16-bits');
1262-
if (len < 128) {
1268+
if (len <= 32) {
12631269
for (let i = 0; i < len; i += 2)
12641270
swap(this, i, i + 1);
12651271
return this;
12661272
}
1267-
return _swap16(this);
1273+
_swap16(this);
1274+
return this;
12681275
};
12691276

12701277
Buffer.prototype.swap32 = function swap32() {
1271-
// For Buffer.length < 192, it's generally faster to
1278+
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
1279+
// For Buffer.length <= 32, it's generally faster to
12721280
// do the swap in javascript. For larger buffers,
12731281
// dropping down to the native code is faster.
1274-
const len = this.length;
1282+
const len = TypedArrayPrototypeGetLength(this);
12751283
if (len % 4 !== 0)
12761284
throw new ERR_INVALID_BUFFER_SIZE('32-bits');
1277-
if (len < 192) {
1285+
if (len <= 32) {
12781286
for (let i = 0; i < len; i += 4) {
12791287
swap(this, i, i + 3);
12801288
swap(this, i + 1, i + 2);
12811289
}
12821290
return this;
12831291
}
1284-
return _swap32(this);
1292+
_swap32(this);
1293+
return this;
12851294
};
12861295

12871296
Buffer.prototype.swap64 = function swap64() {
1288-
// For Buffer.length < 192, it's generally faster to
1297+
// Ref: https://github.com/nodejs/node/pull/61871#discussion_r2889557696
1298+
// For Buffer.length < 48, it's generally faster to
12891299
// do the swap in javascript. For larger buffers,
12901300
// dropping down to the native code is faster.
1291-
const len = this.length;
1301+
// Threshold differs from swap16/swap32 (<=32) because swap64's
1302+
// crossover is between 40 and 48 (native wins at 48, loses at 40).
1303+
const len = TypedArrayPrototypeGetLength(this);
12921304
if (len % 8 !== 0)
12931305
throw new ERR_INVALID_BUFFER_SIZE('64-bits');
1294-
if (len < 192) {
1306+
if (len < 48) {
12951307
for (let i = 0; i < len; i += 8) {
12961308
swap(this, i, i + 7);
12971309
swap(this, i + 1, i + 6);
@@ -1300,7 +1312,8 @@ Buffer.prototype.swap64 = function swap64() {
13001312
}
13011313
return this;
13021314
}
1303-
return _swap64(this);
1315+
_swap64(this);
1316+
return this;
13041317
};
13051318

13061319
Buffer.prototype.toLocaleString = Buffer.prototype.toString;

0 commit comments

Comments
 (0)