Skip to content

Commit

Permalink
buffer: speed up swap16/32, add swap64
Browse files Browse the repository at this point in the history
* Speed up buffer.swap16 and swap32 by using builtins. Up to ~6x gain.
  Drop transition point between JS and C++ implementations accordingly.
  Amount of performance improvement not only depends on buffer size but
  also memory alignment.
* Fix tests: C++ impl tests were testing 0-filled buffers so were
  always passing.
* Add similar buffer.swap64 method.
* Make buffer-swap benchmark mirror JS impl.

doc/api/buffer.markdown has an entry of "added: REPLACEME" that should
be changed to the correct release number before tagged.

Because node is currently using a very old version of cpplint.py it
doesn't know that std::swap() has moved from <algorithm> to <utility> in
c++11. So until cpplint.py is updated simply NOLINT the line.
Technically it should be NOLINT(build/include_what_you_use), but that
puts the line over 80 characters causing another lint error.

PR-URL: #7157
Reviewed-By: Trevor Norris <trev.norris@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
zbjornson authored and trevnorris committed Jun 27, 2016
1 parent 5615487 commit a1059af
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 111 deletions.
75 changes: 52 additions & 23 deletions benchmark/buffers/buffer-swap.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,90 @@
'use strict';

const common = require('../common.js');
const v8 = require('v8');

const bench = common.createBenchmark(main, {
method: ['swap16', 'swap32', 'htons', 'htonl'],
len: [4, 64, 512, 768, 1024, 1536, 2056, 4096, 8192],
n: [1e6]
aligned: ['true', 'false'],
method: ['swap16', 'swap32', 'swap64'/*, 'htons', 'htonl', 'htonll'*/],
len: [8, 64, 128, 256, 512, 768, 1024, 1536, 2056, 4096, 8192],
n: [5e7]
});

// The htons and htonl methods below are used to benchmark the
// performance difference between doing the byteswap in pure
// javascript regardless of Buffer size as opposed to dropping
// down to the native layer for larger Buffer sizes.
// down to the native layer for larger Buffer sizes. Commented
// out by default because they are slow for big buffers. If
// re-evaluating the crossover point, uncomment those methods
// and comment out their implementations in lib/buffer.js so
// C++ version will always be used.

function swap(b, n, m) {
const i = b[n];
b[n] = b[m];
b[m] = i;
}

Buffer.prototype.htons = function htons() {
if (this.length % 2 !== 0)
throw new RangeError();
for (var i = 0, n = 0; i < this.length; i += 2) {
n = this[i];
this[i] = this[i + 1];
this[i + 1] = n;
for (var i = 0; i < this.length; i += 2) {
swap(this, i, i + 1);
}
return this;
};

Buffer.prototype.htonl = function htonl() {
if (this.length % 2 !== 0)
if (this.length % 4 !== 0)
throw new RangeError();
for (var i = 0; i < this.length; i += 4) {
swap(this, i, i + 3);
swap(this, i + 1, i + 2);
}
return this;
};

Buffer.prototype.htonll = function htonl() {
if (this.length % 8 !== 0)
throw new RangeError();
for (var i = 0, n = 0; i < this.length; i += 4) {
n = this[i];
this[i] = this[i + 3];
this[i + 3] = n;
n = this[i + 1];
this[i + 1] = this[i + 2];
this[i + 2] = n;
for (var i = 0; i < this.length; i += 8) {
swap(this, i, i + 7);
swap(this, i + 1, i + 6);
swap(this, i + 2, i + 5);
swap(this, i + 3, i + 4);
}
return this;
};

function createBuffer(len) {
function createBuffer(len, aligned) {
len += aligned ? 0 : 1;
const buf = Buffer.allocUnsafe(len);
for (var i = 1; i <= len; i++)
buf[i - 1] = i;
return buf;
return aligned ? buf : buf.slice(1);
}

function bufferSwap(n, buf, method) {
for (var i = 1; i <= n; i++)
buf[method]();
function genMethod(method) {
const fnString =
'return function ' + method + '(n, buf) {' +
' for (var i = 0; i <= n; i++)' +
' buf.' + method + '();' +
'}';
return (new Function(fnString))();
}

function main(conf) {
const method = conf.method;
const len = conf.len | 0;
const n = conf.n | 0;
const buf = createBuffer(len);
const aligned = conf.aligned || 'true';
const buf = createBuffer(len, aligned === 'true');
const bufferSwap = genMethod(method);

v8.setFlagsFromString('--allow_natives_syntax');
eval('%OptimizeFunctionOnNextCall(bufferSwap)');

bench.start();
bufferSwap(n, buf, method);
bufferSwap(n, buf);
bench.end(n);
}
32 changes: 28 additions & 4 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -1470,10 +1470,10 @@ calls can be chained.
```js
const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]);
console.log(buf);
// Prints Buffer(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8)
// Prints <Buffer 01 02 03 04 05 06 07 08>
buf.swap16();
console.log(buf);
// Prints Buffer(0x2, 0x1, 0x4, 0x3, 0x6, 0x5, 0x8, 0x7)
// Prints <Buffer 02 01 04 03 06 05 08 07>
```

### buf.swap32()
Expand All @@ -1491,12 +1491,36 @@ calls can be chained.
```js
const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]);
console.log(buf);
// Prints Buffer(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8)
// Prints <Buffer 01 02 03 04 05 06 07 08>
buf.swap32();
console.log(buf);
// Prints Buffer(0x4, 0x3, 0x2, 0x1, 0x8, 0x7, 0x6, 0x5)
// Prints <Buffer 04 03 02 01 08 07 06 05>
```

### buf.swap64()
<!-- YAML
added: REPLACEME
-->

* Return: {Buffer}

Interprets the `Buffer` as an array of 64-bit numbers and swaps
the byte-order *in-place*. Throws a `RangeError` if the `Buffer` length is
not a multiple of 64 bits. The method returns a reference to the Buffer, so
calls can be chained.

```js
const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]);
console.log(buf);
// Prints <Buffer 01 02 03 04 05 06 07 08>
buf.swap64();
console.log(buf);
// Prints <Buffer 08 07 06 05 04 03 02 01>
```

Note that JavaScript cannot encode 64-bit integers. This method is intended
for working with 64-bit floats.

### buf.toString([encoding[, start[, end]]])

* `encoding` {String} Default: `'utf8'`
Expand Down
105 changes: 64 additions & 41 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,47 +25,6 @@ var poolSize, poolOffset, allocPool;

binding.setupBufferJS(Buffer.prototype, bindingObj);

const swap16n = binding.swap16;
const swap32n = binding.swap32;

function swap(b, n, m) {
const i = b[n];
b[n] = b[m];
b[m] = i;
}

Buffer.prototype.swap16 = function swap16() {
// For Buffer.length < 512, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
if (len % 2 !== 0)
throw new RangeError('Buffer size must be a multiple of 16-bits');
if (len < 512) {
for (var i = 0; i < len; i += 2)
swap(this, i, i + 1);
return this;
}
return swap16n.apply(this);
};

Buffer.prototype.swap32 = function swap32() {
// For Buffer.length < 1024, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
if (len % 4 !== 0)
throw new RangeError('Buffer size must be a multiple of 32-bits');
if (len < 1024) {
for (var i = 0; i < len; i += 4) {
swap(this, i, i + 3);
swap(this, i + 1, i + 2);
}
return this;
}
return swap32n.apply(this);
};

// |binding.zeroFill| can be undefined when running inside an isolate where we
// do not own the ArrayBuffer allocator. Zero fill is always on in that case.
const zeroFill = bindingObj.zeroFill || [0];
Expand Down Expand Up @@ -1303,3 +1262,67 @@ Buffer.prototype.writeDoubleBE = function writeDoubleBE(val, offset, noAssert) {
binding.writeDoubleBE(this, val, offset, true);
return offset + 8;
};

const swap16n = binding.swap16;
const swap32n = binding.swap32;
const swap64n = binding.swap64;

function swap(b, n, m) {
const i = b[n];
b[n] = b[m];
b[m] = i;
}


Buffer.prototype.swap16 = function swap16() {
// For Buffer.length < 128, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
if (len % 2 !== 0)
throw new RangeError('Buffer size must be a multiple of 16-bits');
if (len < 128) {
for (var i = 0; i < len; i += 2)
swap(this, i, i + 1);
return this;
}
return swap16n(this);
};


Buffer.prototype.swap32 = function swap32() {
// For Buffer.length < 192, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
if (len % 4 !== 0)
throw new RangeError('Buffer size must be a multiple of 32-bits');
if (len < 192) {
for (var i = 0; i < len; i += 4) {
swap(this, i, i + 3);
swap(this, i + 1, i + 2);
}
return this;
}
return swap32n(this);
};


Buffer.prototype.swap64 = function swap64() {
// For Buffer.length < 192, it's generally faster to
// do the swap in javascript. For larger buffers,
// dropping down to the native code is faster.
const len = this.length;
if (len % 8 !== 0)
throw new RangeError('Buffer size must be a multiple of 64-bits');
if (len < 192) {
for (var i = 0; i < len; i += 8) {
swap(this, i, i + 7);
swap(this, i + 1, i + 6);
swap(this, i + 2, i + 5);
swap(this, i + 3, i + 4);
}
return this;
}
return swap64n(this);
};
Loading

0 comments on commit a1059af

Please sign in to comment.