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

Use lookup table for faster hex encoding #245

Merged
merged 2 commits into from
Sep 12, 2019

Conversation

fanatid
Copy link
Contributor

@fanatid fanatid commented Sep 11, 2019

Lookup table faster ~9-12 times than toString('hex') for each byte. See #219


Benchmarks for generating lookup table:
code here
function g11 () {
  var table = []
  for (var i = 0; i < 256; ++i) {
    table[i] = ('0' + i.toString(16)).slice(-2)
  }
  return table
}

function g12 () {
  var table = []
  for (var i = 0; i < 256; ++i) {
    table.push(('0' + i.toString(16)).slice(-2))
  }
  return table
}

function g13 () {
  var table = new Array(256)
  for (var i = 0; i < 256; ++i) {
    table[i] = ('0' + i.toString(16)).slice(-2)
  }
  return table
}

function g211 () {
  var alphabet = '0123456789abcdef'
  var table = []
  for (var i = 0; i < 16; ++i) {
    for (var j = 0; j < 16; ++j) {
      table[i * 16 + j] = alphabet[i] + alphabet[j]
    }
  }
  return table
}

function g212 () {
  var alphabet = '0123456789abcdef'
  var table = []
  for (var i = 0; i < 16; ++i) {
    var i16 = i * 16
    for (var j = 0; j < 16; ++j) {
      table[i16 + j] = alphabet[i] + alphabet[j]
    }
  }
  return table
}

function g22 () {
  var alphabet = '0123456789abcdef'
  var table = []
  for (var i = 0; i < 16; ++i) {
    for (var j = 0; j < 16; ++j) {
      table.push(alphabet[i] + alphabet[j])
    }
  }
  return table
}

function g231 () {
  var alphabet = '0123456789abcdef'
  var table = new Array(256)
  for (var i = 0; i < 16; ++i) {
    for (var j = 0; j < 16; ++j) {
      table[i * 16 + j] = alphabet[i] + alphabet[j]
    }
  }
  return table
}

function g232 () {
  var alphabet = '0123456789abcdef'
  var table = new Array(256)
  for (var i = 0; i < 16; ++i) {
    var i16 = i * 16
    for (var j = 0; j < 16; ++j) {
      table[i16 + j] = alphabet[i] + alphabet[j]
    }
  }
  return table
}

function measure (name, fn, count = 1e3) {
  ;%DeoptimizeFunction(fn)
  ;%NeverOptimizeFunction(fn)

  console.time(name)
  for (var i = 0; i < count; ++i) fn()
  console.timeEnd(name)

  // console.log(%GetOptimizationStatus(fn))
}

measure('toString index', g11)
measure('toString push', g12)
measure('toString index predefined', g13)
measure('array index', g211)
measure('array index i16', g212)
measure('array push', g22)
measure('array predefined', g231)
measure('array predefined i16', g232)


// // not optimized
// $ node --allow-natives-syntax ./t1.js 
// toString index: 52.397ms
// toString push: 51.300ms
// toString index predefined: 46.029ms
// array index: 18.343ms
// array index i16: 17.366ms
// array push: 18.097ms
// array predefined: 17.889ms
// array predefined i16: 15.965ms
// // optimized: kOptimized | kTurboFanned
// $ node --allow-natives-syntax ./t1.js 
// toString index: 32.572ms
// toString push: 34.944ms
// toString index predefined: 27.594ms
// array index: 9.485ms
// array index i16: 8.989ms
// array push: 6.545ms
// array predefined: 9.969ms
// array predefined i16: 8.337ms

If we not allow V8 optimize function (because functions hot, they will be JIT-ed) fastest way:

function g232 () {
  var alphabet = '0123456789abcdef'
  var table = new Array(256)
  for (var i = 0; i < 16; ++i) {
    var i16 = i * 16
    for (var j = 0; j < 16; ++j) {
      table[i16 + j] = alphabet[i] + alphabet[j]
    }
  }
  return table
}

Also this should be packed smi array what should give additional performance (not sure).


Benchmarks for `toString('hex')` new-vs-old:
Code here
// Current Buffer way
function toHex (n) {
  if (n < 16) return '0' + n.toString(16)
  return n.toString(16)
}

function hexSlice (buf, start, end) {
  var len = buf.length

  if (!start || start < 0) start = 0
  if (!end || end < 0 || end > len) end = len

  var out = ''
  for (var i = start; i < end; ++i) {
    out += toHex(buf[i])
  }
  return out
}

// New Buffer way
var hexSliceNewLookupTable = (function () {
  var alphabet = '0123456789abcdef'
  var table = new Array(256)
  for (var i = 0; i < 16; ++i) {
    var i16 = i * 16
    for (var j = 0; j < 16; ++j) {
      table[i16 + j] = alphabet[i] + alphabet[j]
    }
  }
  return table
})()

function hexSliceNew (buf, start, end) {
  var len = buf.length

  if (!start || start < 0) start = 0
  if (!end || end < 0 || end > len) end = len

  var out = ''
  for (var i = start; i < end; ++i) {
    out += hexSliceNewLookupTable[buf[i]]
  }
  return out
}

// Measure it
function measure (name, fn, buffers) {
  console.time(name)
  for (var i = 0; i < buffers.length; ++i) fn(buffers[i])
  console.timeEnd(name)
}

// data
var XorShift128Plus = require('xorshift.js').XorShift128Plus
var prng = new XorShift128Plus('ba4ebf7a16fc931c401f6c7e12c61eaa')
var buffers = new Array(1e4)
for (var i = 0; i < buffers.length; ++i) buffers[i] = prng.randomBytes(1024)

measure('hexSliceOld', hexSlice, buffers)
measure('hexSliceNew', hexSliceNew, buffers)

// // $ node ./t2.js 
// hexSliceOld: 955.224ms
// hexSliceNew: 79.119ms

Current way slowly ~12 times. Tested 10k buffer with 1024 bytes: 995ms vs 79ms.

Lookup table faster ~9-12 times than toString('hex') for each byte.
See feross#219
Copy link
Owner

@feross feross left a comment

Choose a reason for hiding this comment

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

LGTM. An order of magnitude faster is a huge improvement. Nice work.

index.js Outdated Show resolved Hide resolved
@feross feross closed this Sep 12, 2019
@feross feross reopened this Sep 12, 2019
Copy link
Owner

@feross feross left a comment

Choose a reason for hiding this comment

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

Thanks for the PR, merged!

I asked on Twitter and someone suggested a tip that might further improve performance: https://twitter.com/WebReflection/status/1171871844141621249 if you're interested to try to improve it further :)

@feross feross merged commit b172571 into feross:master Sep 12, 2019
@feross
Copy link
Owner

feross commented Sep 12, 2019

5.4.3

@fanatid fanatid deleted the tostring-hex-optimize branch September 12, 2019 05:22
@fanatid
Copy link
Contributor Author

fanatid commented Sep 12, 2019

Oh, forget about Array.from in context of packed-vs-holed array. Not sure how check it whith --allow-native-syntax. I did not used d8, maybe there it's possible to check 🤔
Thanks for the tweet.

@feross
Copy link
Owner

feross commented Sep 12, 2019

@fanatid Of course, no problem. Thanks for your PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants