Skip to content

Commit 7181059

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 4d1557a commit 7181059

File tree

10 files changed

+262
-42
lines changed

10 files changed

+262
-42
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: 65 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+
// Lazily initialized: toHex is installed by InitializeExperimentalGlobal()
59+
// (skipped during snapshot creation) and may be disabled with --no-js-base-64.
60+
let Uint8ArrayPrototypeToHex;
61+
function ensureUint8ArrayToHex() {
62+
if (Uint8ArrayPrototypeToHex === undefined) {
63+
Uint8ArrayPrototypeToHex = Uint8ArrayPrototype.toHex ?
64+
uncurryThis(Uint8ArrayPrototype.toHex) : null;
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,18 @@ function base64ByteLength(str, bytes) {
657682
return (bytes * 3) >>> 2;
658683
}
659684

685+
function hexSliceToHex(buf, start, end) {
686+
ensureUint8ArrayToHex();
687+
// Fall back to C++ when toHex is unavailable or the result would exceed
688+
// kStringMaxLength (so the correct ERR_STRING_TOO_LONG error is thrown).
689+
if (Uint8ArrayPrototypeToHex === null || (end - start) * 2 > kStringMaxLength)
690+
return hexSlice(buf, start, end);
691+
return Uint8ArrayPrototypeToHex(
692+
new Uint8Array(TypedArrayPrototypeGetBuffer(buf),
693+
TypedArrayPrototypeGetByteOffset(buf) + start,
694+
end - start));
695+
}
696+
660697
const encodingOps = {
661698
utf8: {
662699
encoding: 'utf8',
@@ -701,11 +738,7 @@ const encodingOps = {
701738
write: asciiWrite,
702739
slice: asciiSlice,
703740
indexOf: (buf, val, byteOffset, dir) =>
704-
indexOfBuffer(buf,
705-
fromStringFast(val, encodingOps.ascii),
706-
byteOffset,
707-
encodingsMap.ascii,
708-
dir),
741+
indexOfString(buf, val, byteOffset, encodingsMap.latin1, dir),
709742
},
710743
base64: {
711744
encoding: 'base64',
@@ -738,7 +771,7 @@ const encodingOps = {
738771
encodingVal: encodingsMap.hex,
739772
byteLength: (string) => string.length >>> 1,
740773
write: hexWrite,
741-
slice: hexSlice,
774+
slice: hexSliceToHex,
742775
indexOf: (buf, val, byteOffset, dir) =>
743776
indexOfBuffer(buf,
744777
fromStringFast(val, encodingOps.hex),
@@ -1087,7 +1120,7 @@ function _fill(buf, value, offset, end, encoding) {
10871120
value = 0;
10881121
} else if (value.length === 1) {
10891122
// Fast path: If `value` fits into a single byte, use that numeric value.
1090-
if (normalizedEncoding === 'utf8') {
1123+
if (normalizedEncoding === 'utf8' || normalizedEncoding === 'ascii') {
10911124
const code = StringPrototypeCharCodeAt(value, 0);
10921125
if (code < 128) {
10931126
value = code;
@@ -1137,29 +1170,30 @@ function _fill(buf, value, offset, end, encoding) {
11371170
}
11381171

11391172
Buffer.prototype.write = function write(string, offset, length, encoding) {
1173+
const len = this.length;
11401174
// Buffer#write(string);
11411175
if (offset === undefined) {
1142-
return utf8Write(this, string, 0, this.length);
1176+
return utf8Write(this, string, 0, len);
11431177
}
11441178
// Buffer#write(string, encoding)
11451179
if (length === undefined && typeof offset === 'string') {
11461180
encoding = offset;
1147-
length = this.length;
1181+
length = len;
11481182
offset = 0;
11491183

11501184
// Buffer#write(string, offset[, length][, encoding])
11511185
} else {
1152-
validateOffset(offset, 'offset', 0, this.length);
1186+
validateOffset(offset, 'offset', 0, len);
11531187

1154-
const remaining = this.length - offset;
1188+
const remaining = len - offset;
11551189

11561190
if (length === undefined) {
11571191
length = remaining;
11581192
} else if (typeof length === 'string') {
11591193
encoding = length;
11601194
length = remaining;
11611195
} else {
1162-
validateOffset(length, 'length', 0, this.length);
1196+
validateOffset(length, 'length', 0, len);
11631197
if (length > remaining)
11641198
length = remaining;
11651199
}
@@ -1177,9 +1211,10 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
11771211
};
11781212

11791213
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)
1214+
const len = this.length;
1215+
if (len > 0) {
1216+
const data = new Array(len);
1217+
for (let i = 0; i < len; ++i)
11831218
data[i] = this[i];
11841219
return { type: 'Buffer', data };
11851220
}
@@ -1233,7 +1268,8 @@ Buffer.prototype.swap16 = function swap16() {
12331268
swap(this, i, i + 1);
12341269
return this;
12351270
}
1236-
return _swap16(this);
1271+
_swap16(this);
1272+
return this;
12371273
};
12381274

12391275
Buffer.prototype.swap32 = function swap32() {
@@ -1250,7 +1286,8 @@ Buffer.prototype.swap32 = function swap32() {
12501286
}
12511287
return this;
12521288
}
1253-
return _swap32(this);
1289+
_swap32(this);
1290+
return this;
12541291
};
12551292

12561293
Buffer.prototype.swap64 = function swap64() {
@@ -1269,7 +1306,8 @@ Buffer.prototype.swap64 = function swap64() {
12691306
}
12701307
return this;
12711308
}
1272-
return _swap64(this);
1309+
_swap64(this);
1310+
return this;
12731311
};
12741312

12751313
Buffer.prototype.toLocaleString = Buffer.prototype.toString;

src/node_buffer.cc

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,31 +1200,56 @@ int32_t FastIndexOfNumber(Local<Value>,
12001200
static CFunction fast_index_of_number(CFunction::Make(FastIndexOfNumber));
12011201

12021202
void Swap16(const FunctionCallbackInfo<Value>& args) {
1203-
Environment* env = Environment::GetCurrent(args);
1204-
THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]);
12051203
SPREAD_BUFFER_ARG(args[0], ts_obj);
12061204
CHECK(nbytes::SwapBytes16(ts_obj_data, ts_obj_length));
1207-
args.GetReturnValue().Set(args[0]);
12081205
}
12091206

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

12111219
void Swap32(const FunctionCallbackInfo<Value>& args) {
1212-
Environment* env = Environment::GetCurrent(args);
1213-
THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]);
12141220
SPREAD_BUFFER_ARG(args[0], ts_obj);
12151221
CHECK(nbytes::SwapBytes32(ts_obj_data, ts_obj_length));
1216-
args.GetReturnValue().Set(args[0]);
12171222
}
12181223

1224+
void FastSwap32(Local<Value> receiver,
1225+
Local<Value> buffer_obj,
1226+
// NOLINTNEXTLINE(runtime/references)
1227+
FastApiCallbackOptions& options) {
1228+
TRACK_V8_FAST_API_CALL("buffer.swap32");
1229+
HandleScope scope(options.isolate);
1230+
ArrayBufferViewContents<char> buffer(buffer_obj);
1231+
CHECK(nbytes::SwapBytes32(const_cast<char*>(buffer.data()), buffer.length()));
1232+
}
1233+
1234+
static CFunction fast_swap32(CFunction::Make(FastSwap32));
12191235

12201236
void Swap64(const FunctionCallbackInfo<Value>& args) {
1221-
Environment* env = Environment::GetCurrent(args);
1222-
THROW_AND_RETURN_UNLESS_BUFFER(env, args[0]);
12231237
SPREAD_BUFFER_ARG(args[0], ts_obj);
12241238
CHECK(nbytes::SwapBytes64(ts_obj_data, ts_obj_length));
1225-
args.GetReturnValue().Set(args[0]);
12261239
}
12271240

1241+
void FastSwap64(Local<Value> receiver,
1242+
Local<Value> buffer_obj,
1243+
// NOLINTNEXTLINE(runtime/references)
1244+
FastApiCallbackOptions& options) {
1245+
TRACK_V8_FAST_API_CALL("buffer.swap64");
1246+
HandleScope scope(options.isolate);
1247+
ArrayBufferViewContents<char> buffer(buffer_obj);
1248+
CHECK(nbytes::SwapBytes64(const_cast<char*>(buffer.data()), buffer.length()));
1249+
}
1250+
1251+
static CFunction fast_swap64(CFunction::Make(FastSwap64));
1252+
12281253
static void IsUtf8(const FunctionCallbackInfo<Value>& args) {
12291254
Environment* env = Environment::GetCurrent(args);
12301255
CHECK_EQ(args.Length(), 1);
@@ -1622,9 +1647,9 @@ void Initialize(Local<Object> target,
16221647
SetMethodNoSideEffect(
16231648
context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer);
16241649

1625-
SetMethod(context, target, "swap16", Swap16);
1626-
SetMethod(context, target, "swap32", Swap32);
1627-
SetMethod(context, target, "swap64", Swap64);
1650+
SetFastMethod(context, target, "swap16", Swap16, &fast_swap16);
1651+
SetFastMethod(context, target, "swap32", Swap32, &fast_swap32);
1652+
SetFastMethod(context, target, "swap64", Swap64, &fast_swap64);
16281653

16291654
SetMethodNoSideEffect(context, target, "isUtf8", IsUtf8);
16301655
SetMethodNoSideEffect(context, target, "isAscii", IsAscii);
@@ -1693,8 +1718,11 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
16931718
registry->Register(IndexOfString);
16941719

16951720
registry->Register(Swap16);
1721+
registry->Register(fast_swap16);
16961722
registry->Register(Swap32);
1723+
registry->Register(fast_swap32);
16971724
registry->Register(Swap64);
1725+
registry->Register(fast_swap64);
16981726

16991727
registry->Register(IsUtf8);
17001728
registry->Register(IsAscii);

0 commit comments

Comments
 (0)