【问题标题】:Sorting is very slow if sort order is explicitly defined如果明确定义排序顺序,排序会非常慢
【发布时间】:2017-05-23 21:11:21
【问题描述】:

正如您可以在下面的代码中查看的那样,通过在属性名称前加上 +- 来定义排序顺序会导致排序速度大大降低。任何想法为什么?

默认排序(值):5.84ms

升序(+值):58.59ms

降序排序(-value):49.28ms

value+value 之间存在差异(两者都返回完全相同的排序顺序)让我非常困惑。

let arr = []

for (let i = 0; i < 10000; ++i) {
  arr.push({ _id: 'doc' + i, value: Math.random(), k: i % 20 })
}

function sort (...keys) {
  let data = {}
  data.sort = []
  keys.forEach(key => {
    const newKey = key.replace(/(\-|\+)/g, '')
    const order = key[0] === '-' ? -1 : 1
    data.sort.push({
      key: newKey,
      order: order
    })
  })
  return data
}

function compare (sortData, a, b) {
  for (let i = 0; i < sortData.length; ++i) {
    const data = sortData[i]
    const _a = a[data.key]
    const _b = b[data.key]
    if (_a !== _b) return _a > _b ? data.order : -data.order
  }
  return 0
}

function exec (data) {
  let r = arr.slice()
  if (data.sort) {
    r.sort((a, b) => {
      let r = compare(data.sort, a, b)
      return r
    })
  }
  return r
}

function time (f) {
  let start = performance.now()
  f()
  return performance.now() - start
}

function test (f, n) {
  let total = 0
  for (let i = 0; i < n; ++i) total += time(f)
  return total / n
}

const q1 = sort('value')
const q2 = sort('+value')
const q3 = sort('-value')
const sortDefault = () => exec(q1)
const sortAsc = () => exec(q2)
const sortDesc = () => exec(q3)

console.log('    Default Sort (value):', test(sortDefault, 10).toFixed(2) + 'ms')
console.log(' Ascending Sort (+value):', test(sortAsc, 10).toFixed(2) + 'ms')
console.log('Descending Sort (-value):', test(sortDesc, 10).toFixed(2) + 'ms')

【问题讨论】:

  • 不确定性能差异,但这是在 JS 中编写比较器的一种奇怪方式。编写一个返回然后传递给Array.prototype.sort的函数会更常见。
  • 对我来说差不多是同一时间:Default Sort (value): 6.60ms Ascending Sort (+value): 9.45ms Descending Sort (-value): 7.39ms
  • 奇怪的是,我在 Firefox 中也遇到了同样的困难时间,但 IE/Edge/Chrome/NodeJS 都导致时间增加。
  • Chrome 似乎是最大的不同。只是一个猜测,但我认为这与字段访问中的破坏优化有关,他们能够静态确定"value" 永远不会被key.replace(/(\-|\+)/g, '') 更改,因此他们能够使用他们的低级类表示但与其他人一样,它默认为对象的哈希表表示。当引擎无法静态确定表达式的值时,通过[...] 访问键可以做到这一点。
  • 如果这对性能至关重要,那么找到一种方法而不是将顺序编码到密钥中可能不是一个坏主意。

标签: javascript node.js sorting


【解决方案1】:

编写一个返回比较器的函数会更常见,尽管在这种情况下,性能差异似乎取决于 V8 JS 引擎在以下情况下如何优化代码正则表达式匹配。

因此,更规范的用法可能是:

function buildSort(...keys) {

  if (typeof keys[0] === "string") {
    keys = keys.map(key => {
        const order = key[0] === '-' ? -1 : 1
        key = key.replace(/(\-|\+)/g, '')
        return { key, order };
    })
  }

  return (a, b) => {
    for (let { key, order } of keys ) {
      const _a = a[key];
      const _b = b[key];
      if (_a !== _b) return _a > _b ? order : -order
    }
    return 0
  }
}

function exec(comparator) {
  return arr.slice().sort(comparator);
}

...

const q1 = buildSort({ key: 'value', order: 1 });
const q2 = buildSort({ key: 'value', order: -1 });
const q3 = buildSort('+value')

在运行 NodeJS 6.9.1 的笔记本电脑上,通过 { key: 'value', order: 1 }"value" 运行时间为 0.015 秒,而通过 "+value" 运行时间为 0.46 秒,尽管名义上这些结果与 keys 中出现的值相同比较器中使用的结构。

【讨论】:

    【解决方案2】:

    这更多是作为评论的意思,但对于一个人来说太长了:对于一个测试场景来查看如果密钥稍后被实习是否存在 V8 性能差异,我尝试存储一个原始密钥(正则表达式的结果) 并在第一次比较时获取对象的实际键(理论上应该是一个内部或至少可优化的字符串)。

    下面是相同的代码,但是在排序函数中设置了一个属性rawkey而不是'key',并且在与if(!data.key) data.key = Object.keys(a).filter(k=&gt;k==data.rawkey)[0];比较的内部获得了key 无论好坏,这在 Chrome 中为我创造了相同的性能。 该实现只是一个展示案例,并不安全,并非所有对象都可能具有实际属性

    let arr = []
    
    for (let i = 0; i < 10000; ++i) {
      arr.push({ _id: 'doc' + i, value: Math.random(), k: i % 20 })
    }
    
    function sort (...keys) {
      let data = {}
      data.sort = []
      keys.forEach(key => {
        const newKey = key.replace(/(\-|\+)/g, '')
        const order = key[0] === '-' ? -1 : 1
        data.sort.push({
          rawkey: newKey,
          order: order
        })
      })
      return data
    }
    
    function compare (sortData, a, b) {
      for (let i = 0; i < sortData.length; ++i) {
        const data = sortData[i];
        if(!data.key) data.key = Object.keys(a).filter(k=>k==data.rawkey)[0];
        const _a = a[data.key]
        const _b = b[data.key]
        if (_a !== _b) return _a > _b ? data.order : -data.order
      }
      return 0
    }
    
    function exec (data) {
      let r = arr.slice()
      if (data.sort) {
        r.sort((a, b) => {
          let r = compare(data.sort, a, b)
          return r
        })
      }
      return r
    }
    
    function time (f) {
      let start = performance.now()
      f()
      return performance.now() - start
    }
    
    function test (f, n) {
      let total = 0
      for (let i = 0; i < n; ++i) total += time(f)
      return total / n
    }
    
    const q1 = sort('value')
    const q2 = sort('+value')
    const q3 = sort('-value')
    const sortDefault = () => exec(q1)
    const sortAsc = () => exec(q2)
    const sortDesc = () => exec(q3)
    
    console.log('    Default Sort (value):', test(sortDefault, 10).toFixed(2) + 'ms')
    console.log(' Ascending Sort (+value):', test(sortAsc, 10).toFixed(2) + 'ms')
    console.log('Descending Sort (-value):', test(sortDesc, 10).toFixed(2) + 'ms')

    【讨论】:

    • FWIW,作为“扩展评论”,您可以像我一样做这个“社区 Wiki”。也不清楚您的 Object.keys() 代码实际上对引擎盖下的优化器做了什么。
    【解决方案3】:

    我发现动态构建比较函数可以避免减速,并且在我的测试中几乎所有情况下(尽管是最小的)实际上都快得多。

          Default ( value): 5.47ms
          Default (+value): 54.17ms
          Default (-value): 51.90ms
    eval Function ( value): 3.25ms
    eval Function (+value): 3.51ms
    eval Function (-value): 2.99ms
    

    let arr = []
    for (let i = 0; i < 10000; ++i) {
      let o = { _id: 'doc' + i, value: Math.random() }
      arr.push(o)
    }
    
    function sortKeys (...keys) {
      const data = {}
      data.sort = []
      keys.forEach(key => {
        if (typeof key === 'string') key = getSortObject(key)
        if (key.order === undefined) key.order = 1
        data.sort.push(key)
      })
      return data
    }
    
    function getSortObject (str) {
      if (str.match(/^(\+|\-)/)) {
        return { key: str.substr(1), order: -1 }
      } else {
        return { key: str, order: 1 }
      }
    }
    
    function exec (data, compFn) {
      let r = arr.slice()
      if (data.sort) {
        r.sort(compFn(data.sort))
      }
      return r
    }
    
    // Old comparator, seems to cause optimization problems in the V8 engine. (Chrome/NodeJS/etc.)
    function comparison (sdata) {
      return function (a, b) {
        for (let i = 0; i < sdata.length; ++i) {
          const data = sdata[i]
          const _a = a[data.key]
          const _b = b[data.key]
          if (_a !== _b) return _a > _b ? data.order : -data.order
        }
        return 0
      }
    }
    
    // Builds a function from the sort keys.
    function getCompFunc (sortData) {
      let str = ''
      sortData.forEach(sort => {
        str += `if(a.${sort.key}${sort.order === 1 ? '>' : '<'}b.${sort.key})return 1;`
        str += `if(a.${sort.key}${sort.order === 1 ? '<' : '>'}b.${sort.key})return -1;`
      })
      str += `return 0;`
      return Function('a', 'b', str)
    }
    
    function time (f) {
      let start = performance.now()
      f()
      return performance.now() - start
    }
    
    function test (f, n) {
      let total = 0
      for (let i = 0; i < n; ++i) total += time(f)
      return total / n
    }
    
    const q1 = sortKeys('value')
    const q2 = sortKeys('+value')
    const q3 = sortKeys('-value')
    const sortDefault = () => exec(q1, comparison)
    const sortAsc = () => exec(q2, comparison)
    const sortDesc = () => exec(q3, comparison)
    const sortDefaultEval = () => exec(q1, getCompFunc)
    const sortAscEval = () => exec(q2, getCompFunc)
    const sortDescEval = () => exec(q3, getCompFunc)
    
    console.log('A:', test(sortDefault, 5).toFixed(2) + 'ms')
    console.log('B:', test(sortAsc, 5).toFixed(2) + 'ms')
    console.log('C:', test(sortDesc, 5).toFixed(2) + 'ms')
    console.log('-')
    console.log('D:', test(sortDefaultEval, 5).toFixed(2) + 'ms')
    console.log('E:', test(sortAscEval, 5).toFixed(2) + 'ms')
    console.log('F:', test(sortDescEval, 5).toFixed(2) + 'ms')

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-09-27
      • 1970-01-01
      • 2015-05-12
      • 2016-05-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多