From 54885d45de7af1ed328b027f3123d7dd837e699f Mon Sep 17 00:00:00 2001 From: "Christopher Jeffrey (JJ)" Date: Wed, 31 Jan 2024 19:54:40 -0500 Subject: [PATCH] Optimize hexWrite with an array lookup table (#343) Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com> --- index.js | 67 ++++++++++++++++++++++++----------------------- package.json | 2 +- perf/write-hex.js | 24 +++++++++++++++++ test/write-hex.js | 59 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 34 deletions(-) create mode 100644 perf/write-hex.js create mode 100644 test/write-hex.js diff --git a/index.js b/index.js index dad9152b..b0b51298 100644 --- a/index.js +++ b/index.js @@ -855,19 +855,24 @@ function hexWrite (buf, string, offset, length) { const strLen = string.length - if (length > strLen / 2) { - length = strLen / 2 + if (length > (strLen >>> 1)) { + length = strLen >>> 1 } - let i - for (i = 0; i < length; ++i) { - const a = hexCharValueTable[string[i * 2]] - const b = hexCharValueTable[string[i * 2 + 1]] - if (a === undefined || b === undefined) { + + for (let i = 0; i < length; ++i) { + const a = string.charCodeAt(i * 2 + 0) + const b = string.charCodeAt(i * 2 + 1) + const hi = hexCharValueTable[a & 0x7f] + const lo = hexCharValueTable[b & 0x7f] + + if ((a | b | hi | lo) & ~0x7f) { return i } - buf[offset + i] = a << 4 | b + + buf[offset + i] = (hi << 4) | lo } - return i + + return length } function utf8Write (buf, string, offset, length) { @@ -2118,30 +2123,26 @@ const hexSliceLookupTable = (function () { })() // hex lookup table for Buffer.from(x, 'hex') -const hexCharValueTable = { - 0: 0, - 1: 1, - 2: 2, - 3: 3, - 4: 4, - 5: 5, - 6: 6, - 7: 7, - 8: 8, - 9: 9, - a: 10, - b: 11, - c: 12, - d: 13, - e: 14, - f: 15, - A: 10, - B: 11, - C: 12, - D: 13, - E: 14, - F: 15 -} +/* eslint-disable no-multi-spaces, indent */ +const hexCharValueTable = [ + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1 +] +/* eslint-enable no-multi-spaces, indent */ // Return not function with Error if BigInt not supported function defineBigIntMethod (fn) { diff --git a/package.json b/package.json index 536fe46c..5ee6b8d9 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ }, "scripts": { "perf": "browserify --debug perf/bracket-notation.js > perf/bundle.js && open perf/index.html", - "perf-node": "node perf/bracket-notation.js && node perf/concat.js && node perf/copy-big.js && node perf/copy.js && node perf/new-big.js && node perf/new.js && node perf/readDoubleBE.js && node perf/readFloatBE.js && node perf/readUInt32LE.js && node perf/slice.js && node perf/writeFloatBE.js", + "perf-node": "node perf/bracket-notation.js && node perf/concat.js && node perf/copy-big.js && node perf/copy.js && node perf/new-big.js && node perf/new.js && node perf/readDoubleBE.js && node perf/readFloatBE.js && node perf/readUInt32LE.js && node perf/slice.js && node perf/writeFloatBE.js && node perf/write-hex.js", "size": "browserify -r ./ | uglifyjs -c -m | gzip | wc -c", "standard": "standard", "test": "tape test/*.js test/node/*.js", diff --git a/perf/write-hex.js b/perf/write-hex.js new file mode 100644 index 00000000..76b03be5 --- /dev/null +++ b/perf/write-hex.js @@ -0,0 +1,24 @@ +const BrowserBuffer = require('../').Buffer // (this module) +const util = require('./util') +const suite = util.suite() + +const LENGTH = 4096 +const browserSubject = BrowserBuffer.alloc(LENGTH) +const nodeSubject = Buffer.alloc(LENGTH) + +const charset = '0123456789abcdef' + +let str = '' + +for (let i = 0; i < LENGTH * 2; i++) + str += charset[Math.random() * charset.length | 0] + +suite + .add('BrowserBuffer#write(' + LENGTH + ', "hex")', function () { + browserSubject.write(str, 'hex') + }) + +if (!process.browser) suite + .add('NodeBuffer#write(' + LENGTH + ', "hex")', function () { + nodeSubject.write(str, 'hex') + }) diff --git a/test/write-hex.js b/test/write-hex.js new file mode 100644 index 00000000..c10ac2b2 --- /dev/null +++ b/test/write-hex.js @@ -0,0 +1,59 @@ +'use strict' + +const Buffer = require('../').Buffer +const test = require('tape') + +test('buffer.write("hex") should stop on invalid characters', function (t) { + // Test the entire 16-bit space. + for (let ch = 0; ch <= 0xffff; ch++) { + // 0-9 + if (ch >= 0x30 && ch <= 0x39) { + continue + } + + // A-F + if (ch >= 0x41 && ch <= 0x46) { + continue + } + + // a-f + if (ch >= 0x61 && ch <= 0x66) { + continue + } + + for (const str of [ + 'abcd' + String.fromCharCode(ch) + 'ef0', + 'abcde' + String.fromCharCode(ch) + 'f0', + 'abcd' + String.fromCharCode(ch + 0) + String.fromCharCode(ch + 1) + 'f0', + 'abcde' + String.fromCharCode(ch + 0) + String.fromCharCode(ch + 1) + '0' + ]) { + const buf = Buffer.alloc(4) + t.equal(str.length, 8) + t.equal(buf.write(str, 'hex'), 2) + t.equal(buf.toString('hex'), 'abcd0000') + t.equal(Buffer.from(str, 'hex').toString('hex'), 'abcd') + } + } + + t.end() +}) + +test('buffer.write("hex") should truncate odd string lengths', function (t) { + const buf = Buffer.alloc(32) + const charset = '0123456789abcdef' + + let str = '' + + for (let i = 0; i < 63; i++) { + str += charset[Math.random() * charset.length | 0] + } + + t.equal(buf.write('abcde', 'hex'), 2) + t.equal(buf.toString('hex', 0, 3), 'abcd00') + + buf.fill(0) + + t.equal(buf.write(str, 'hex'), 31) + t.equal(buf.toString('hex', 0, 32), str.slice(0, -1) + '00') + t.end() +})