Skip to content

Commit

Permalink
lib: hand-optimize Buffer constructor
Browse files Browse the repository at this point in the history
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: #1048
Reviewed-By: Trevor Norris <trev.norris@gmail.com>
  • Loading branch information
bnoordhuis committed Mar 5, 2015
1 parent 3446ff4 commit bbf54a5
Showing 1 changed file with 140 additions and 74 deletions.
214 changes: 140 additions & 74 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit bbf54a5

Please sign in to comment.