From bbf54a554ababa91ced243acb0d2721bc19eb1dc Mon Sep 17 00:00:00 2001 From: Ben Noordhuis Date: Wed, 4 Mar 2015 00:50:30 +0100 Subject: [PATCH] lib: hand-optimize Buffer constructor The Buffer constructor is used pervasively throughout io.js, yet it was one of the most unwieldy functions in core. This commit breaks up the constructor into several small functions in a way that makes V8 happy. About 8-10% CPU time was attributed to the constructor function before in buffer-heavy benchmarks. That pretty much drops to zero now because V8 can now easily inline it at the call site. It shortens the running time of the following simple benchmark by about 15%: for (var i = 0; i < 25e6; ++i) new Buffer(1); And about 8% from this benchmark: for (var i = 0; i < 1e7; ++i) new Buffer('x', 'ucs2'); PR-URL: https://github.com/iojs/io.js/pull/1048 Reviewed-By: Trevor Norris --- lib/buffer.js | 214 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 140 insertions(+), 74 deletions(-) diff --git a/lib/buffer.js b/lib/buffer.js index b5b1e9650de982..ba832de975db4d 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -24,82 +24,148 @@ function createPool() { } createPool(); +function Buffer(arg) { + if (!(this instanceof Buffer)) { + // Avoid going through an ArgumentsAdaptorTrampoline in the common case. + if (arguments.length > 1) + return new Buffer(arg, arguments[1]); -function Buffer(subject, encoding) { - if (!(this instanceof Buffer)) - return new Buffer(subject, encoding); - - if (typeof subject === 'number') { - this.length = +subject; - - } else if (typeof subject === 'string') { - if (typeof encoding !== 'string' || encoding.length === 0) - encoding = 'utf8'; - this.length = Buffer.byteLength(subject, encoding); + return new Buffer(arg); + } - // Handle Arrays, Buffers, Uint8Arrays or JSON. - } else if (subject !== null && typeof subject === 'object') { - if (subject.type === 'Buffer' && Array.isArray(subject.data)) - subject = subject.data; - this.length = +subject.length; + this.length = 0; + this.parent = undefined; - } else { - throw new TypeError('must start with number, buffer, array or string'); + // Common case. + if (typeof(arg) === 'number') { + fromNumber(this, arg); + return; } - if (this.length > kMaxLength) { - throw new RangeError('Attempt to allocate Buffer larger than maximum ' + - 'size: 0x' + kMaxLength.toString(16) + ' bytes'); + // Slightly less common case. + if (typeof(arg) === 'string') { + fromString(this, arg, arguments.length > 1 ? arguments[1] : 'utf8'); + return; } - if (this.length < 0) - this.length = 0; - else - this.length >>>= 0; // Coerce to uint32. + // Unusual. + fromObject(this, arg); +} - this.parent = undefined; - if (this.length <= (Buffer.poolSize >>> 1) && this.length > 0) { - if (this.length > poolSize - poolOffset) - createPool(); - this.parent = sliceOnto(allocPool, - this, - poolOffset, - poolOffset + this.length); - poolOffset += this.length; - } else { - alloc(this, this.length); - } +function fromNumber(that, length) { + allocate(that, length < 0 ? 0 : checked(length) | 0); +} - if (typeof subject === 'number') { - return; +function fromString(that, string, encoding) { + if (typeof(encoding) !== 'string' || encoding === '') + encoding = 'utf8'; + + // Assumption: byteLength() return value is always < kMaxLength. + var length = byteLength(string, encoding) | 0; + allocate(that, length); + + var actual = that.write(string, encoding) | 0; + if (actual !== length) { + // Fix up for truncated base64 input. Don't bother returning + // the unused two or three bytes to the pool. + that.length = actual; + truncate(that, actual); } +} - if (typeof subject === 'string') { - // In the case of base64 it's possible that the size of the buffer - // allocated was slightly too large. In this case we need to rewrite - // the length to the actual length written. - var len = this.write(subject, encoding); - // Buffer was truncated after decode, realloc internal ExternalArray - if (len !== this.length) { - var prevLen = this.length; - this.length = len; - truncate(this, this.length); - // Only need to readjust the poolOffset if the allocation is a slice. - if (this.parent != undefined) - poolOffset -= (prevLen - len); - } +function fromObject(that, object) { + if (object instanceof Buffer) + return fromBuffer(that, object); + + if (Array.isArray(object)) + return fromArray(that, object); + + if (object == null) + throw new TypeError('must start with number, buffer, array or string'); - } else if (subject instanceof Buffer) { - subject.copy(this, 0, 0, this.length); + if (object.buffer instanceof ArrayBuffer) + return fromTypedArray(that, object); + + if (object.length) + return fromArrayLike(that, object); + + return fromJsonObject(that, object); +} + +function fromBuffer(that, buffer) { + var length = checked(buffer.length) | 0; + allocate(that, length); + buffer.copy(that, 0, 0, length); +} + +function fromArray(that, array) { + var length = checked(array.length) | 0; + allocate(that, length); + for (var i = 0; i < length; i += 1) + that[i] = array[i] & 255; +} - } else if (typeof subject.length === 'number' || Array.isArray(subject)) { - // Really crappy way to handle Uint8Arrays, but V8 doesn't give a simple - // way to access the data from the C++ API. - for (var i = 0; i < this.length; i++) - this[i] = subject[i]; +// Duplicate of fromArray() to keep fromArray() monomorphic. +function fromTypedArray(that, array) { + var length = checked(array.length) | 0; + allocate(that, length); + // Truncating the elements is probably not what people expect from typed + // arrays with BYTES_PER_ELEMENT > 1 but it's compatible with the behavior + // of the old Buffer constructor. + for (var i = 0; i < length; i += 1) + that[i] = array[i] & 255; +} + +function fromArrayLike(that, array) { + var length = checked(array.length) | 0; + allocate(that, length); + for (var i = 0; i < length; i += 1) + that[i] = array[i] & 255; +} + +// Deserialize { type: 'Buffer', data: [1,2,3,...] } into a Buffer object. +// Returns a zero-length buffer for inputs that don't conform to the spec. +function fromJsonObject(that, object) { + var array; + var length = 0; + + if (object.type === 'Buffer' && Array.isArray(object.data)) { + array = object.data; + length = checked(array.length) | 0; } + allocate(that, length); + + for (var i = 0; i < length; i += 1) + that[i] = array[i] & 255; +} + +function allocate(that, length) { + var fromPool = length !== 0 && length <= Buffer.poolSize >>> 1; + that.parent = fromPool ? palloc(that, length) : alloc(that, length); + that.length = length; } +function palloc(that, length) { + if (length > poolSize - poolOffset) + createPool(); + + var start = poolOffset; + var end = start + length; + var buf = sliceOnto(allocPool, that, start, end); + poolOffset = end; + + return buf; +} + +function checked(length) { + // Note: cannot use `length < kMaxLength` here because that fails when + // length is NaN (which is otherwise coerced to zero.) + if (length >= kMaxLength) { + throw new RangeError('Attempt to allocate Buffer larger than maximum ' + + 'size: 0x' + kMaxLength.toString(16) + ' bytes'); + } + return length >>> 0; +} function SlowBuffer(length) { length = length >>> 0; @@ -197,30 +263,30 @@ Buffer.concat = function(list, length) { }; -Buffer.byteLength = function(str, enc) { - var ret; - str = str + ''; - switch (enc) { +function byteLength(string, encoding) { + if (typeof(string) !== 'string') + string = String(string); + + switch (encoding) { case 'ascii': case 'binary': case 'raw': - ret = str.length; - break; + return string.length; + case 'ucs2': case 'ucs-2': case 'utf16le': case 'utf-16le': - ret = str.length * 2; - break; + return string.length * 2; + case 'hex': - ret = str.length >>> 1; - break; - default: - ret = binding.byteLength(str, enc); + return string.length >>> 1; } - return ret; -}; + return binding.byteLength(string, encoding); +} + +Buffer.byteLength = byteLength; // toString(encoding, start=0, end=buffer.length) Buffer.prototype.toString = function(encoding, start, end) {