delvedor / find-my-way

A crazy fast HTTP router

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

fast-decode-uri-component isn't really fast

mav-rik opened this issue · comments

fast-decode-uri-component isn't really fast

During my investigation of #206 problem I was playing with fast-decode-uri-component in order to create fast-decode-uri that would work exactly as decodeURI. During my own benchmarking I was never able to beat the standard decodeURI. The reason for that, as I found later, was the test data.

In fast-decode-uri-component they use a lot of malformed URIs for benchmarking. The standard decodeURIComponent processes malformed URIs extremely slow. But when you use proper URIs the default function works 2-3 times faster than the "fast" one.

How many URIs are malformed on a highly loaded API? The only way to get malformed URI is to manually mess it up in browser or misconfigure a new integration (which eventually will be fixed). It's quite counterproductive to optimize the framework to process malformed URIs faster and process proper URIs slower.

My suggestion is to get rid of the fast-decode-uri-component and use safe and a little fastified version of the standard function like I show in benchmark below. That will work 2-3 times faster on properly formatted URIs.

The benchmark:

const bench = require('nanobench')
const fastDecode = require('fast-decode-uri-component')

const uri = [ 
  encodeURIComponent(' /?!#@=[](),\'"'),
  encodeURIComponent('много русских букв'),
  encodeURIComponent('acde=bdfd'),
  encodeURIComponent('這裡有些話'),
  encodeURIComponent('कुछ शब्द यहाँ'),
  encodeURIComponent('algunas palabras aquí'),
  encodeURIComponent('✌👀🎠🎡🍺')
]

function safeDecodeURIComponent(uri) {
  if (uri.indexOf('%') < 0) return uri
  try {
    return decodeURIComponent(uri)
  } catch (e) {
    return uri // or it can be null
  }
}

const uriLen = uri.length

bench('fast-decode-uri-component', b => {
  b.start()
  for (var round = 0; round < 100000; round++) {
    for (var i = 0; i < uriLen; i++) {
      fastDecode(uri[i])
    }
  }
  b.end()
})

bench('decodeURIComponent', b => {
  b.start()
  for (var round = 0; round < 100000; round++) {
    for (var i = 0; i < uriLen; i++) {
      safeDecodeURIComponent(uri[i])
    }
  }
  b.end()
})

As a result the standard function works 2.8 times faster:

# fast-decode-uri-component
ok ~512 ms (0 s + 511994600 ns)

# decodeURIComponent
ok ~182 ms (0 s + 182000400 ns)

all benchmarks completed
ok ~694 ms (0 s + 693995000 ns)

Thanks and good spot! Would you like to send a PR for this?

My results are the following (the winner is highlighted)

// ' /?!#@=[](),\'"'
+safeDecodeURIComponent(0) x 4,782,218 ops/sec ±0.51% (597 runs sampled)
fastDecode(0) x 3,132,525 ops/sec ±0.10% (596 runs sampled)

// 'много русских букв'
+safeDecodeURIComponent(1) x 2,398,408 ops/sec ±0.16% (594 runs sampled)
fastDecode(1) x 873,075 ops/sec ±0.18% (595 runs sampled)

// 'acde=bdfd'
safeDecodeURIComponent(2) x 5,266,314 ops/sec ±0.09% (595 runs sampled)
+fastDecode(2) x 17,789,412 ops/sec ±0.19% (589 runs sampled)

// '這裡有些話'
+safeDecodeURIComponent(3) x 4,783,379 ops/sec ±0.13% (595 runs sampled)
fastDecode(3) x 2,170,871 ops/sec ±0.11% (596 runs sampled)

// 'कुछ शब्द यहाँ'
+safeDecodeURIComponent(4) x 2,875,314 ops/sec ±0.13% (595 runs sampled)
fastDecode(4) x 853,179 ops/sec ±0.10% (593 runs sampled)

// 'algunas palabras aquí'
safeDecodeURIComponent(5) x 3,247,450 ops/sec ±0.11% (596 runs sampled)
+fastDecode(5) x 7,176,086 ops/sec ±0.24% (596 runs sampled)

// '✌👀🎠🎡🍺'
+safeDecodeURIComponent(6) x 3,400,513 ops/sec ±0.10% (596 runs sampled)
fastDecode(6) x 1,698,406 ops/sec ±0.12% (596 runs sampled)
Bench script!
'use strict'

const Benchmark = require('benchmark')
const fastDecode = require('fast-decode-uri-component')
Benchmark.options.minSamples = 500

const suite = Benchmark.Suite()

const uri = [
  encodeURIComponent(' /?!#@=[](),\'"'),
  encodeURIComponent('много русских букв'),
  encodeURIComponent('acde=bdfd'),
  encodeURIComponent('這裡有些話'),
  encodeURIComponent('कुछ शब्द यहाँ'),
  encodeURIComponent('algunas palabras aquí'),
  encodeURIComponent('✌👀🎠🎡🍺')
]

function safeDecodeURIComponent (uri) {
  if (uri.indexOf('%') < 0) return uri
  try {
    return decodeURIComponent(uri)
  } catch (e) {
    return null // or it can be null
  }
}

uri.forEach(function (u, i) {
  suite.add(`safeDecodeURIComponent(${i})`, function () {
    safeDecodeURIComponent(u)
  })
  suite.add(`fastDecode(${i})`, function () {
    fastDecode(u)
  })
})
suite
  .on('cycle', function (event) {
    console.log(String(event.target))
  })
  .on('complete', function () {})
  .run()

And if we combine the two?

function safeDecodeURIComponent (uri) {
  if (uri.indexOf('%') < 0) return uri
  try {
    return fastDecode(uri)
  } catch (e) {
    return null // or it can be null
  }
}

All the string has the % in this script, the results are almost the same

fastDecode(0) x 3,132,311 ops/sec ±0.15% (597 runs sampled)
safeFastDecode(0) x 3,140,256 ops/sec ±0.10% (596 runs sampled)

fastDecode(1) x 875,180 ops/sec ±0.10% (595 runs sampled)
safeFastDecode(1) x 873,825 ops/sec ±0.10% (595 runs sampled)

fastDecode(2) x 17,879,316 ops/sec ±0.23% (589 runs sampled)
safeFastDecode(2) x 17,764,888 ops/sec ±0.19% (589 runs sampled)

fastDecode(3) x 2,174,047 ops/sec ±0.11% (596 runs sampled)
safeFastDecode(3) x 2,171,648 ops/sec ±0.12% (595 runs sampled)

fastDecode(4) x 849,655 ops/sec ±0.12% (596 runs sampled)
safeFastDecode(4) x 849,826 ops/sec ±0.10% (596 runs sampled)

fastDecode(5) x 7,211,117 ops/sec ±0.11% (596 runs sampled)
safeFastDecode(5) x 7,203,678 ops/sec ±0.10% (596 runs sampled)

fastDecode(6) x 1,697,746 ops/sec ±0.09% (596 runs sampled)
safeFastDecode(6) x 1,696,386 ops/sec ±0.09% (596 runs sampled)

And using a for loop that check for char code 37 (%) using .charCodeAt(i)?

And using a for loop that check for char code 37 (%) using .charCodeAt(i)?

I was checking that and found that nothing is faster than indexOf.

All the string has the % in this script, the results are almost the same

If I read your benchmark shortly, the combo of the two is equal of the best case of both. Specifically there was acde=bdfd which was faster with fastDecode.