Skip to content

Commit

Permalink
buffer: optimize from() and byteLength()
Browse files Browse the repository at this point in the history
PR-URL: #12361
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
  • Loading branch information
mscdex authored and addaleax committed Apr 14, 2017
1 parent 46f2026 commit 4a86803
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 86 deletions.
34 changes: 20 additions & 14 deletions benchmark/buffers/buffer-bytelength.js
Expand Up @@ -2,7 +2,7 @@
var common = require('../common'); var common = require('../common');


var bench = common.createBenchmark(main, { var bench = common.createBenchmark(main, {
encoding: ['utf8', 'base64'], encoding: ['utf8', 'base64', 'buffer'],
len: [1, 2, 4, 16, 64, 256], // x16 len: [1, 2, 4, 16, 64, 256], // x16
n: [5e6] n: [5e6]
}); });
Expand All @@ -21,21 +21,27 @@ function main(conf) {
var encoding = conf.encoding; var encoding = conf.encoding;


var strings = []; var strings = [];
for (var string of chars) { var results;
// Strings must be built differently, depending on encoding if (encoding === 'buffer') {
var data = buildString(string, len); strings = [ Buffer.alloc(len * 16, 'a') ];
if (encoding === 'utf8') { results = [ len * 16 ];
strings.push(data); } else {
} else if (encoding === 'base64') { for (var string of chars) {
// Base64 strings will be much longer than their UTF8 counterparts // Strings must be built differently, depending on encoding
strings.push(Buffer.from(data, 'utf8').toString('base64')); var data = buildString(string, len);
if (encoding === 'utf8') {
strings.push(data);
} else if (encoding === 'base64') {
// Base64 strings will be much longer than their UTF8 counterparts
strings.push(Buffer.from(data, 'utf8').toString('base64'));
}
} }
}


// Check the result to ensure it is *properly* optimized // Check the result to ensure it is *properly* optimized
var results = strings.map(function(val) { results = strings.map(function(val) {
return Buffer.byteLength(val, encoding); return Buffer.byteLength(val, encoding);
}); });
}


bench.start(); bench.start();
for (var i = 0; i < n; i++) { for (var i = 0; i < n; i++) {
Expand Down
10 changes: 9 additions & 1 deletion benchmark/buffers/buffer-from.js
Expand Up @@ -10,11 +10,12 @@ const bench = common.createBenchmark(main, {
'buffer', 'buffer',
'uint8array', 'uint8array',
'string', 'string',
'string-utf8',
'string-base64', 'string-base64',
'object' 'object'
], ],
len: [10, 2048], len: [10, 2048],
n: [1024] n: [2048]
}); });


function main(conf) { function main(conf) {
Expand Down Expand Up @@ -75,6 +76,13 @@ function main(conf) {
} }
bench.end(n); bench.end(n);
break; break;
case 'string-utf8':
bench.start();
for (i = 0; i < n * 1024; i++) {
Buffer.from(str, 'utf8');
}
bench.end(n);
break;
case 'string-base64': case 'string-base64':
bench.start(); bench.start();
for (i = 0; i < n * 1024; i++) { for (i = 0; i < n * 1024; i++) {
Expand Down
156 changes: 90 additions & 66 deletions lib/buffer.js
Expand Up @@ -23,8 +23,7 @@


const binding = process.binding('buffer'); const binding = process.binding('buffer');
const { compare: compare_, compareOffset } = binding; const { compare: compare_, compareOffset } = binding;
const { isArrayBuffer, isSharedArrayBuffer, isUint8Array } = const { isAnyArrayBuffer, isUint8Array } = process.binding('util');
process.binding('util');
const bindingObj = {}; const bindingObj = {};
const internalUtil = require('internal/util'); const internalUtil = require('internal/util');


Expand Down Expand Up @@ -116,16 +115,19 @@ function Buffer(arg, encodingOrOffset, length) {
* Buffer.from(arrayBuffer[, byteOffset[, length]]) * Buffer.from(arrayBuffer[, byteOffset[, length]])
**/ **/
Buffer.from = function(value, encodingOrOffset, length) { Buffer.from = function(value, encodingOrOffset, length) {
if (typeof value === 'number') if (typeof value === 'string')
throw new TypeError('"value" argument must not be a number'); return fromString(value, encodingOrOffset);


if (isArrayBuffer(value) || isSharedArrayBuffer(value)) if (isAnyArrayBuffer(value))
return fromArrayBuffer(value, encodingOrOffset, length); return fromArrayBuffer(value, encodingOrOffset, length);


if (typeof value === 'string') var b = fromObject(value);
return fromString(value, encodingOrOffset); if (b)
return b;


return fromObject(value); if (typeof value === 'number')
throw new TypeError('"value" argument must not be a number');
throw new TypeError(kFromErrorMsg);
}; };


Object.setPrototypeOf(Buffer, Uint8Array); Object.setPrototypeOf(Buffer, Uint8Array);
Expand Down Expand Up @@ -218,24 +220,27 @@ function allocate(size) {




function fromString(string, encoding) { function fromString(string, encoding) {
if (typeof encoding !== 'string' || encoding === '') var length;
if (typeof encoding !== 'string' || encoding.length === 0) {
encoding = 'utf8'; encoding = 'utf8';

if (string.length === 0)
if (!Buffer.isEncoding(encoding)) return new FastBuffer();
throw new TypeError('"encoding" must be a valid string encoding'); length = binding.byteLengthUtf8(string);

} else {
if (string.length === 0) length = byteLength(string, encoding, true);
return new FastBuffer(); if (length === -1)

throw new TypeError('"encoding" must be a valid string encoding');
var length = byteLength(string, encoding); if (string.length === 0)
return new FastBuffer();
}


if (length >= (Buffer.poolSize >>> 1)) if (length >= (Buffer.poolSize >>> 1))
return binding.createFromString(string, encoding); return binding.createFromString(string, encoding);


if (length > (poolSize - poolOffset)) if (length > (poolSize - poolOffset))
createPool(); createPool();
var b = new FastBuffer(allocPool, poolOffset, length); var b = new FastBuffer(allocPool, poolOffset, length);
var actual = b.write(string, encoding); const actual = b.write(string, encoding);
if (actual !== length) { if (actual !== length) {
// byteLength() may overestimate. That's a rare case, though. // byteLength() may overestimate. That's a rare case, though.
b = new FastBuffer(allocPool, poolOffset, actual); b = new FastBuffer(allocPool, poolOffset, actual);
Expand All @@ -255,8 +260,14 @@ function fromArrayLike(obj) {


function fromArrayBuffer(obj, byteOffset, length) { function fromArrayBuffer(obj, byteOffset, length) {
// convert byteOffset to integer // convert byteOffset to integer
byteOffset = +byteOffset; if (byteOffset === undefined) {
byteOffset = byteOffset ? Math.trunc(byteOffset) : 0; byteOffset = 0;
} else {
byteOffset = +byteOffset;
// check for NaN
if (byteOffset !== byteOffset)
byteOffset = 0;
}


const maxLength = obj.byteLength - byteOffset; const maxLength = obj.byteLength - byteOffset;


Expand All @@ -268,11 +279,17 @@ function fromArrayBuffer(obj, byteOffset, length) {
} else { } else {
// convert length to non-negative integer // convert length to non-negative integer
length = +length; length = +length;
length = length ? Math.trunc(length) : 0; // Check for NaN
length = length <= 0 ? 0 : Math.min(length, Number.MAX_SAFE_INTEGER); if (length !== length) {

length = 0;
if (length > maxLength) } else if (length > 0) {
throw new RangeError("'length' is out of bounds"); length = (length < Number.MAX_SAFE_INTEGER ?
length : Number.MAX_SAFE_INTEGER);
if (length > maxLength)
throw new RangeError("'length' is out of bounds");
} else {
length = 0;
}
} }


return new FastBuffer(obj, byteOffset, length); return new FastBuffer(obj, byteOffset, length);
Expand All @@ -289,9 +306,8 @@ function fromObject(obj) {
return b; return b;
} }


if (obj) { if (obj != undefined) {
if (obj.length !== undefined || isArrayBuffer(obj.buffer) || if (obj.length !== undefined || isAnyArrayBuffer(obj.buffer)) {
isSharedArrayBuffer(obj.buffer)) {
if (typeof obj.length !== 'number' || obj.length !== obj.length) { if (typeof obj.length !== 'number' || obj.length !== obj.length) {
return new FastBuffer(); return new FastBuffer();
} }
Expand All @@ -302,8 +318,6 @@ function fromObject(obj) {
return fromArrayLike(obj.data); return fromArrayLike(obj.data);
} }
} }

throw new TypeError(kFromErrorMsg);
} }




Expand Down Expand Up @@ -388,53 +402,63 @@ function base64ByteLength(str, bytes) {


function byteLength(string, encoding) { function byteLength(string, encoding) {
if (typeof string !== 'string') { if (typeof string !== 'string') {
if (ArrayBuffer.isView(string) || isArrayBuffer(string) || if (ArrayBuffer.isView(string) || isAnyArrayBuffer(string)) {
isSharedArrayBuffer(string)) {
return string.byteLength; return string.byteLength;
} }


throw new TypeError('"string" must be a string, Buffer, or ArrayBuffer'); throw new TypeError('"string" must be a string, Buffer, or ArrayBuffer');
} }


var len = string.length; const len = string.length;
if (len === 0) const mustMatch = (arguments.length > 2 && arguments[2] === true);
if (!mustMatch && len === 0)
return 0; return 0;


// Use a for loop to avoid recursion if (!encoding)
var loweredCase = false; return (mustMatch ? -1 : binding.byteLengthUtf8(string));
for (;;) {
switch (encoding) { encoding += '';
case 'ascii': switch (encoding.length) {
case 'latin1': case 4:
case 'binary': if (encoding === 'utf8') return binding.byteLengthUtf8(string);
return len; if (encoding === 'ucs2') return len * 2;

encoding = encoding.toLowerCase();
case 'utf8': if (encoding === 'utf8') return binding.byteLengthUtf8(string);
case 'utf-8': if (encoding === 'ucs2') return len * 2;
case undefined: break;
return binding.byteLengthUtf8(string); case 5:

if (encoding === 'utf-8') return binding.byteLengthUtf8(string);
case 'ucs2': if (encoding === 'ascii') return len;
case 'ucs-2': if (encoding === 'ucs-2') return len * 2;
case 'utf16le': encoding = encoding.toLowerCase();
case 'utf-16le': if (encoding === 'utf-8') return binding.byteLengthUtf8(string);
if (encoding === 'ascii') return len;
if (encoding === 'ucs-2') return len * 2;
break;
case 7:
if (encoding === 'utf16le' || encoding.toLowerCase() === 'utf16le')
return len * 2; return len * 2;

break;
case 'hex': case 8:
if (encoding === 'utf-16le' || encoding.toLowerCase() === 'utf-16le')
return len * 2;
break;
case 6:
if (encoding === 'latin1' || encoding === 'binary') return len;
if (encoding === 'base64') return base64ByteLength(string, len);
encoding = encoding.toLowerCase();
if (encoding === 'latin1' || encoding === 'binary') return len;
if (encoding === 'base64') return base64ByteLength(string, len);
break;
case 3:
if (encoding === 'hex' || encoding.toLowerCase() === 'hex')
return len >>> 1; return len >>> 1;

break;
case 'base64':
return base64ByteLength(string, len);

default:
// The C++ binding defaulted to UTF8, we should too.
if (loweredCase)
return binding.byteLengthUtf8(string);

encoding = ('' + encoding).toLowerCase();
loweredCase = true;
}
} }
if (mustMatch)
throw new TypeError('Unknown encoding: ' + encoding);
else
return binding.byteLengthUtf8(string);
} }


Buffer.byteLength = byteLength; Buffer.byteLength = byteLength;
Expand Down
5 changes: 2 additions & 3 deletions lib/util.js
Expand Up @@ -452,7 +452,7 @@ function formatValue(ctx, value, recurseTimes) {
// Fast path for ArrayBuffer and SharedArrayBuffer. // Fast path for ArrayBuffer and SharedArrayBuffer.
// Can't do the same for DataView because it has a non-primitive // Can't do the same for DataView because it has a non-primitive
// .buffer property that we need to recurse for. // .buffer property that we need to recurse for.
if (binding.isArrayBuffer(value) || binding.isSharedArrayBuffer(value)) { if (binding.isAnyArrayBuffer(value)) {
return `${constructor.name}` + return `${constructor.name}` +
` { byteLength: ${formatNumber(ctx, value.byteLength)} }`; ` { byteLength: ${formatNumber(ctx, value.byteLength)} }`;
} }
Expand Down Expand Up @@ -494,8 +494,7 @@ function formatValue(ctx, value, recurseTimes) {
keys.unshift('size'); keys.unshift('size');
empty = value.size === 0; empty = value.size === 0;
formatter = formatMap; formatter = formatMap;
} else if (binding.isArrayBuffer(value) || } else if (binding.isAnyArrayBuffer(value)) {
binding.isSharedArrayBuffer(value)) {
braces = ['{', '}']; braces = ['{', '}'];
keys.unshift('byteLength'); keys.unshift('byteLength');
visibleKeys.byteLength = true; visibleKeys.byteLength = true;
Expand Down
10 changes: 8 additions & 2 deletions src/node_util.cc
Expand Up @@ -20,7 +20,6 @@ using v8::Value;




#define VALUE_METHOD_MAP(V) \ #define VALUE_METHOD_MAP(V) \
V(isArrayBuffer, IsArrayBuffer) \
V(isDataView, IsDataView) \ V(isDataView, IsDataView) \
V(isDate, IsDate) \ V(isDate, IsDate) \
V(isExternal, IsExternal) \ V(isExternal, IsExternal) \
Expand All @@ -30,7 +29,6 @@ using v8::Value;
V(isRegExp, IsRegExp) \ V(isRegExp, IsRegExp) \
V(isSet, IsSet) \ V(isSet, IsSet) \
V(isSetIterator, IsSetIterator) \ V(isSetIterator, IsSetIterator) \
V(isSharedArrayBuffer, IsSharedArrayBuffer) \
V(isTypedArray, IsTypedArray) \ V(isTypedArray, IsTypedArray) \
V(isUint8Array, IsUint8Array) V(isUint8Array, IsUint8Array)


Expand All @@ -44,6 +42,12 @@ using v8::Value;
VALUE_METHOD_MAP(V) VALUE_METHOD_MAP(V)
#undef V #undef V


static void IsAnyArrayBuffer(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(1, args.Length());
args.GetReturnValue().Set(
args[0]->IsArrayBuffer() || args[0]->IsSharedArrayBuffer());
}

static void GetPromiseDetails(const FunctionCallbackInfo<Value>& args) { static void GetPromiseDetails(const FunctionCallbackInfo<Value>& args) {
// Return undefined if it's not a Promise. // Return undefined if it's not a Promise.
if (!args[0]->IsPromise()) if (!args[0]->IsPromise())
Expand Down Expand Up @@ -151,6 +155,8 @@ void Initialize(Local<Object> target,
VALUE_METHOD_MAP(V) VALUE_METHOD_MAP(V)
#undef V #undef V


env->SetMethod(target, "isAnyArrayBuffer", IsAnyArrayBuffer);

#define V(name, _) \ #define V(name, _) \
target->Set(context, \ target->Set(context, \
FIXED_ONE_BYTE_STRING(env->isolate(), #name), \ FIXED_ONE_BYTE_STRING(env->isolate(), #name), \
Expand Down

0 comments on commit 4a86803

Please sign in to comment.