Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

buffer: add swap16() and swap32() methods #5724

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions benchmark/buffers/buffer-swap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict';

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

const bench = common.createBenchmark(main, {
method: ['swap16', 'swap32', 'htons', 'htonl'],
len: [4, 64, 512, 768, 1024, 1536, 2056, 4096, 8192],
n: [1e6]
});

// 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.

Buffer.prototype.htons = function htons() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kinda confusing. htons is for converting between byte orders over the wire

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the term is used only in the benchmark and was selected because it's common for the operation. It's not actually used anywhere in the regular code or API.

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;
}
return this;
};

Buffer.prototype.htonl = function htonl() {
if (this.length % 2 !== 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;
}
return this;
};

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

function bufferSwap(n, buf, method) {
for (var i = 1; i <= n; i++)
buf[method]();
}

function main(conf) {
const method = conf.method;
const len = conf.len | 0;
const n = conf.n | 0;
const buf = createBuffer(len);
bench.start();
bufferSwap(n, buf, method);
bench.end(n);
}
36 changes: 36 additions & 0 deletions doc/api/buffer.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,42 @@ buf.slice(-5, -2).toString();
// Returns 'uff', equivalent to buf.slice(1, 4)
```

### buf.swap16()

* Return: {Buffer}

Interprets the `Buffer` as an array of unsigned 16-bit integers and swaps
the byte-order *in-place*. Throws a `RangeError` if the `Buffer` length is
not a multiple of 16 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(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8)
buf.swap16();
console.log(buf);
// Prints Buffer(0x2, 0x1, 0x4, 0x3, 0x6, 0x5, 0x8, 0x7)
```

### buf.swap32()

* Return: {Buffer}

Interprets the `Buffer` as an array of unsigned 32-bit integers and swaps
the byte-order *in-place*. Throws a `RangeError` if the `Buffer` length is
not a multiple of 32 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(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8)
buf.swap32();
console.log(buf);
// Prints Buffer(0x4, 0x3, 0x2, 0x1, 0x8, 0x7, 0x6, 0x5)
```

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

* `encoding` {String} Default: `'utf8'`
Expand Down
42 changes: 42 additions & 0 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,48 @@ 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 length must be a multiple of 16-bits');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incredible nitpicking, but nonetheless I would opt for one of these:

Buffer size must be a multiple of 16 bits

or:

Buffer length must be a multiple of 2

I hope you understand the nuance difference I'm getting at.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will change to Buffer size must be a multiple of 16 bits when I land.

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 length 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);
};

const flags = bindingObj.flags;
const kNoZeroFill = 0;

Expand Down
32 changes: 32 additions & 0 deletions src/node_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@
#define BUFFER_MALLOC(length) \
zero_fill_all_buffers ? calloc(length, 1) : malloc(length)

#define SWAP_BYTES(arr, a, b) \
do { \
const uint8_t lo = arr[a]; \
arr[a] = arr[b]; \
arr[b] = lo; \
} while (0)

namespace node {

// if true, all Buffer and SlowBuffer instances will automatically zero-fill
Expand Down Expand Up @@ -1092,6 +1099,28 @@ void IndexOfNumber(const FunctionCallbackInfo<Value>& args) {
: -1);
}

void Swap16(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_UNLESS_BUFFER(env, args.This());
SPREAD_ARG(args.This(), ts_obj);

for (size_t i = 0; i < ts_obj_length; i += 2) {
SWAP_BYTES(ts_obj_data, i, i + 1);
}
args.GetReturnValue().Set(args.This());
}

void Swap32(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_UNLESS_BUFFER(env, args.This());
SPREAD_ARG(args.This(), ts_obj);

for (size_t i = 0; i < ts_obj_length; i += 4) {
SWAP_BYTES(ts_obj_data, i, i + 3);
SWAP_BYTES(ts_obj_data, i + 1, i + 2);
}
args.GetReturnValue().Set(args.This());
}

// pass Buffer object to load prototype methods
void SetupBufferJS(const FunctionCallbackInfo<Value>& args) {
Expand Down Expand Up @@ -1158,6 +1187,9 @@ void Initialize(Local<Object> target,
env->SetMethod(target, "writeFloatBE", WriteFloatBE);
env->SetMethod(target, "writeFloatLE", WriteFloatLE);

env->SetMethod(target, "swap16", Swap16);
env->SetMethod(target, "swap32", Swap32);

target->Set(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "kMaxLength"),
Integer::NewFromUnsigned(env->isolate(), kMaxLength)).FromJust();
Expand Down
60 changes: 60 additions & 0 deletions test/parallel/test-buffer-swap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

require('../common');
const assert = require('assert');

const buf = Buffer.from([0x1, 0x2, 0x3, 0x4]);

assert.strictEqual(buf, buf.swap16());
assert.deepStrictEqual(buf, Buffer.from([0x2, 0x1, 0x4, 0x3]));

assert.strictEqual(buf, buf.swap32());
assert.deepStrictEqual(buf, Buffer.from([0x3, 0x4, 0x1, 0x2]));

const buf_array = [];
for (var i = 1; i < 33; i++)
buf_array.push(i);
const buf2 = Buffer.from(buf_array);
buf2.swap32();
assert.deepStrictEqual(buf2,
Buffer.from([0x04, 0x03, 0x02, 0x01, 0x08, 0x07, 0x06, 0x05, 0x0c,
0x0b, 0x0a, 0x09, 0x10, 0x0f, 0x0e, 0x0d, 0x14, 0x13,
0x12, 0x11, 0x18, 0x17, 0x16, 0x15, 0x1c, 0x1b, 0x1a,
0x19, 0x20, 0x1f, 0x1e, 0x1d]));
buf2.swap16();
assert.deepStrictEqual(buf2,
Buffer.from([0x03, 0x04, 0x01, 0x02, 0x07, 0x08, 0x05, 0x06, 0x0b,
0x0c, 0x09, 0x0a, 0x0f, 0x10, 0x0d, 0x0e, 0x13, 0x14,
0x11, 0x12, 0x17, 0x18, 0x15, 0x16, 0x1b, 0x1c, 0x19,
0x1a, 0x1f, 0x20, 0x1d, 0x1e]));

const buf3 = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]);
buf3.slice(1, 5).swap32();
assert.deepStrictEqual(buf3, Buffer.from([0x1, 0x5, 0x4, 0x3, 0x2, 0x6, 0x7]));

buf3.slice(1, 5).swap16();
assert.deepStrictEqual(buf3, Buffer.from([0x1, 0x4, 0x5, 0x2, 0x3, 0x6, 0x7]));

// Force use of native code (Buffer size above threshold limit for js impl)
const buf4 = Buffer.allocUnsafe(1024).fill([0x1, 0x2, 0x3, 0x4]);
const buf5 = Buffer.allocUnsafe(1024).fill([0x2, 0x1, 0x4, 0x3]);
const buf6 = Buffer.allocUnsafe(1024)
.fill([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]);
const buf7 = Buffer.allocUnsafe(1024)
.fill([0x4, 0x3, 0x2, 0x1, 0x8, 0x7, 0x6, 0x5]);

buf4.swap16();
assert.deepStrictEqual(buf4, buf5);

buf6.swap32();
assert.deepStrictEqual(buf6, buf7);


const re16 = /Buffer length must be a multiple of 16-bits/;
const re32 = /Buffer length must be a multiple of 32-bits/;

assert.throws(() => Buffer.from(buf3).swap16(), re16);
assert.throws(() => Buffer.alloc(1025).swap16(), re16);
assert.throws(() => Buffer.from(buf3).swap32(), re32);
assert.throws(() => buf3.slice(1, 3).swap32(), re32);
assert.throws(() => Buffer.alloc(1025).swap32(), re32);