Skip to content

Commit d6c9fec

Browse files
committed
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
1 parent 35d3bc8 commit d6c9fec

9 files changed

Lines changed: 212 additions & 33 deletions

File tree

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,11 +50,22 @@ const {
5050
TypedArrayPrototypeGetByteOffset,
5151
TypedArrayPrototypeGetLength,
5252
TypedArrayPrototypeSet,
53-
TypedArrayPrototypeSlice,
5453
TypedArrayPrototypeSubarray,
5554
Uint8Array,
55+
Uint8ArrayPrototype,
56+
uncurryThis,
5657
} = primordials;
5758

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

397+
const byteLength = TypedArrayPrototypeGetByteLength(view);
398+
386399
if (offset !== undefined || length !== undefined) {
387400
if (offset !== undefined) {
388401
validateInteger(offset, 'offset', 0);
389402
if (offset >= viewLength) return new FastBuffer();
390403
} else {
391404
offset = 0;
392405
}
406+
393407
let end;
394408
if (length !== undefined) {
395409
validateInteger(length, 'length', 0);
396-
end = offset + length;
410+
end = MathMin(offset + length, viewLength);
397411
} else {
398412
end = viewLength;
399413
}
400414

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

404428
return fromArrayLike(new Uint8Array(
405429
TypedArrayPrototypeGetBuffer(view),
406430
TypedArrayPrototypeGetByteOffset(view),
407-
TypedArrayPrototypeGetByteLength(view)));
431+
byteLength));
408432
};
409433

410434
// Identical to the built-in %TypedArray%.of(), but avoids using the deprecated
@@ -551,14 +575,15 @@ function fromArrayBuffer(obj, byteOffset, length) {
551575
}
552576

553577
function fromArrayLike(obj) {
554-
if (obj.length <= 0)
578+
const len = obj.length;
579+
if (len <= 0)
555580
return new FastBuffer();
556-
if (obj.length < (Buffer.poolSize >>> 1)) {
557-
if (obj.length > (poolSize - poolOffset))
581+
if (len < (Buffer.poolSize >>> 1)) {
582+
if (len > (poolSize - poolOffset))
558583
createPool();
559-
const b = new FastBuffer(allocPool, poolOffset, obj.length);
584+
const b = new FastBuffer(allocPool, poolOffset, len);
560585
TypedArrayPrototypeSet(b, obj, 0);
561-
poolOffset += obj.length;
586+
poolOffset += len;
562587
alignPool();
563588
return b;
564589
}
@@ -688,6 +713,14 @@ function base64ByteLength(str, bytes) {
688713
return (bytes * 3) >>> 2;
689714
}
690715

716+
function hexSliceToHex(buf, start, end) {
717+
ensureUint8ArrayToHex();
718+
return Uint8ArrayPrototypeToHex(
719+
new Uint8Array(TypedArrayPrototypeGetBuffer(buf),
720+
TypedArrayPrototypeGetByteOffset(buf) + start,
721+
end - start));
722+
}
723+
691724
const encodingOps = {
692725
utf8: {
693726
encoding: 'utf8',
@@ -732,11 +765,7 @@ const encodingOps = {
732765
write: asciiWrite,
733766
slice: asciiSlice,
734767
indexOf: (buf, val, byteOffset, dir) =>
735-
indexOfBuffer(buf,
736-
fromStringFast(val, encodingOps.ascii),
737-
byteOffset,
738-
encodingsMap.ascii,
739-
dir),
768+
indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir),
740769
},
741770
base64: {
742771
encoding: 'base64',
@@ -769,7 +798,7 @@ const encodingOps = {
769798
encodingVal: encodingsMap.hex,
770799
byteLength: (string) => string.length >>> 1,
771800
write: hexWrite,
772-
slice: hexSlice,
801+
slice: hexSliceToHex,
773802
indexOf: (buf, val, byteOffset, dir) =>
774803
indexOfBuffer(buf,
775804
fromStringFast(val, encodingOps.hex),
@@ -1118,7 +1147,7 @@ function _fill(buf, value, offset, end, encoding) {
11181147
value = 0;
11191148
} else if (value.length === 1) {
11201149
// Fast path: If `value` fits into a single byte, use that numeric value.
1121-
if (normalizedEncoding === 'utf8') {
1150+
if (normalizedEncoding === 'utf8' || normalizedEncoding === 'ascii') {
11221151
const code = StringPrototypeCharCodeAt(value, 0);
11231152
if (code < 128) {
11241153
value = code;
@@ -1168,29 +1197,30 @@ function _fill(buf, value, offset, end, encoding) {
11681197
}
11691198

11701199
Buffer.prototype.write = function write(string, offset, length, encoding) {
1200+
const len = this.length;
11711201
// Buffer#write(string);
11721202
if (offset === undefined) {
1173-
return utf8Write(this, string, 0, this.length);
1203+
return utf8Write(this, string, 0, len);
11741204
}
11751205
// Buffer#write(string, encoding)
11761206
if (length === undefined && typeof offset === 'string') {
11771207
encoding = offset;
1178-
length = this.length;
1208+
length = len;
11791209
offset = 0;
11801210

11811211
// Buffer#write(string, offset[, length][, encoding])
11821212
} else {
1183-
validateOffset(offset, 'offset', 0, this.length);
1213+
validateOffset(offset, 'offset', 0, len);
11841214

1185-
const remaining = this.length - offset;
1215+
const remaining = len - offset;
11861216

11871217
if (length === undefined) {
11881218
length = remaining;
11891219
} else if (typeof length === 'string') {
11901220
encoding = length;
11911221
length = remaining;
11921222
} else {
1193-
validateOffset(length, 'length', 0, this.length);
1223+
validateOffset(length, 'length', 0, len);
11941224
if (length > remaining)
11951225
length = remaining;
11961226
}
@@ -1208,9 +1238,10 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
12081238
};
12091239

12101240
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)
1241+
const len = this.length;
1242+
if (len > 0) {
1243+
const data = new Array(len);
1244+
for (let i = 0; i < len; ++i)
12141245
data[i] = this[i];
12151246
return { type: 'Buffer', data };
12161247
}
@@ -1264,7 +1295,8 @@ Buffer.prototype.swap16 = function swap16() {
12641295
swap(this, i, i + 1);
12651296
return this;
12661297
}
1267-
return _swap16(this);
1298+
_swap16(this);
1299+
return this;
12681300
};
12691301

12701302
Buffer.prototype.swap32 = function swap32() {
@@ -1281,7 +1313,8 @@ Buffer.prototype.swap32 = function swap32() {
12811313
}
12821314
return this;
12831315
}
1284-
return _swap32(this);
1316+
_swap32(this);
1317+
return this;
12851318
};
12861319

12871320
Buffer.prototype.swap64 = function swap64() {
@@ -1300,7 +1333,8 @@ Buffer.prototype.swap64 = function swap64() {
13001333
}
13011334
return this;
13021335
}
1303-
return _swap64(this);
1336+
_swap64(this);
1337+
return this;
13041338
};
13051339

13061340
Buffer.prototype.toLocaleString = Buffer.prototype.toString;

src/node_buffer.cc

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,16 @@ 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()), buffer.length()));
1217+
}
1218+
1219+
static CFunction fast_swap16(CFunction::Make(FastSwap16));
12101220

12111221
void Swap32(const FunctionCallbackInfo<Value>& args) {
12121222
Environment* env = Environment::GetCurrent(args);
@@ -1216,6 +1226,16 @@ void Swap32(const FunctionCallbackInfo<Value>& args) {
12161226
args.GetReturnValue().Set(args[0]);
12171227
}
12181228

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

12201240
void Swap64(const FunctionCallbackInfo<Value>& args) {
12211241
Environment* env = Environment::GetCurrent(args);
@@ -1225,6 +1245,17 @@ void Swap64(const FunctionCallbackInfo<Value>& args) {
12251245
args.GetReturnValue().Set(args[0]);
12261246
}
12271247

1248+
void FastSwap64(Local<Value> receiver,
1249+
Local<Value> buffer_obj,
1250+
// NOLINTNEXTLINE(runtime/references)
1251+
FastApiCallbackOptions& options) {
1252+
HandleScope scope(options.isolate);
1253+
ArrayBufferViewContents<char> buffer(buffer_obj);
1254+
CHECK(nbytes::SwapBytes64(const_cast<char*>(buffer.data()), buffer.length()));
1255+
}
1256+
1257+
static CFunction fast_swap64(CFunction::Make(FastSwap64));
1258+
12281259
static void IsUtf8(const FunctionCallbackInfo<Value>& args) {
12291260
Environment* env = Environment::GetCurrent(args);
12301261
CHECK_EQ(args.Length(), 1);
@@ -1622,9 +1653,9 @@ void Initialize(Local<Object> target,
16221653
SetMethodNoSideEffect(
16231654
context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer);
16241655

1625-
SetMethod(context, target, "swap16", Swap16);
1626-
SetMethod(context, target, "swap32", Swap32);
1627-
SetMethod(context, target, "swap64", Swap64);
1656+
SetFastMethod(context, target, "swap16", Swap16, &fast_swap16);
1657+
SetFastMethod(context, target, "swap32", Swap32, &fast_swap32);
1658+
SetFastMethod(context, target, "swap64", Swap64, &fast_swap64);
16281659

16291660
SetMethodNoSideEffect(context, target, "isUtf8", IsUtf8);
16301661
SetMethodNoSideEffect(context, target, "isAscii", IsAscii);
@@ -1693,8 +1724,11 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
16931724
registry->Register(IndexOfString);
16941725

16951726
registry->Register(Swap16);
1727+
registry->Register(fast_swap16);
16961728
registry->Register(Swap32);
1729+
registry->Register(fast_swap32);
16971730
registry->Register(Swap64);
1731+
registry->Register(fast_swap64);
16981732

16991733
registry->Register(IsUtf8);
17001734
registry->Register(IsAscii);

0 commit comments

Comments
 (0)