通过将a.localeCompare(b)改为(a<b?-1:(a>b?1:0)),可以提高400倍的排序速度。

61

通过将JavaScript排序函数从

myArray.sort(function (a, b) {
  return a.name.localeCompare(b.name);
});

myArray.sort(function (a, b) {
  return (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0));
});

我成功地将在Chrome中对一个大约有1700个元素的数组进行排序所需的时间从1993毫秒减少到了5毫秒。速度提升了近400倍。但不幸的是,这是以无法正确排序非英语字符串为代价的。

显然,当我尝试进行排序时,我不能让我的用户界面(UI)阻塞2秒钟。有没有什么办法可以避免可怕缓慢的localeCompare函数,同时仍然支持本地化字符串的排序?


2
考虑将Web Worker分离出来,以异步方式执行基于localeCompare的排序。您可能会发现,序列化和反序列化大量数据所花费的时间超过了异步执行的好处,但这值得一试。 - Matt Ball
那可能会起作用,但是2秒仍然非常慢以显示结果。 - Brad Dwyer
你可以考虑另一种方法——从一开始就保持列表排序,这样就不需要显式地对其进行排序。数据来自哪里?JavaScript已经实现了一些自排序数据结构:http://stackoverflow.com/a/5309821/139010 或 https://dev59.com/31DTa4cB1Zd3GeqPL8SU#3809836。 - Matt Ball
这来自Facebook。我们最终在他们需要访问之前预加载并排序了它。 - Brad Dwyer
1
@MattBall,Web Workers似乎不能像其他代码一样处理localeCompare。请参见此问题 - redbmk
2
请注意,localeCompare函数在比较时不区分大小写(或者可能取决于用户的语言环境?在我的电脑上设置为en_US时是不区分大小写的)。而您的替换代码是区分大小写的,因此"Foo"会排在"bar"之前。 - Kip
5个回答

58

通过预先声明Collator对象并使用其compare方法,可以获得显着的性能提升。例如:

const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
arrayOfObjects.sort((a, b) => {
  return collator.compare(a.name, b.name);
});

注意:如果元素是浮点数,则此方法无效。请查看此处的解释

这里有一个基准测试脚本,比较了三种方法:

const arr = [];
for (let i = 0; i < 2000; i++) {
  arr.push(`test-${Math.random()}`);
}

const arr1 = arr.slice();
const arr2 = arr.slice();
const arr3 = arr.slice();

console.time('#1 - localeCompare');
arr1.sort((a, b) => a.localeCompare(
  b,
  undefined, {
    numeric: true,
    sensitivity: 'base'
  }
));
console.timeEnd('#1 - localeCompare');

console.time('#2 - collator');
const collator = new Intl.Collator('en', {
  numeric: true,
  sensitivity: 'base'
});
arr2.sort((a, b) => collator.compare(a, b));
console.timeEnd('#2 - collator');

console.time('#3 - non-locale');
arr3.sort((a, b) => (a < b ? -1 : (a > b ? 1 : 0)));
console.timeEnd('#3 - non-locale');


4
@BradDwyer,我编辑了答案并加入了基准测试脚本。 - Andy
不错!在 Chrome 69 上,我的速度仅比 localeCompare 版本慢了 15 倍,而后者则慢了 800 倍。(非 locale 版本为 1.70 毫秒,Collator 为 25.96 毫秒,localeCompare 为 1380.65 毫秒) - Brad Dwyer
@Andy,那段代码有一个错误。测试#3重复使用了arr2,这个数组应该已经被前面的测试排序过了,所以第三个测试结果会出现人为的快速排序。最好在每个测试中调用arr.slice().sort(...)。但更重要的是,#3应该对参数调用toLocaleLowercase()以进行公平比较。否则,它将产生与前两个测试不同的排序顺序。 - jdunning
@junning 我修复了第三个数组的拼写错误。速度仍然很快。第三个测试用例的重点是概述非区域比较。它肯定不会返回相同的结果,但对于某些不需要区域设置(例如:对英文名称列表进行排序)的用例可能已经足够了。 - Andy
在IE 11中,对一个特定的500项数组进行排序,时间从40秒降至不到1秒。 - Karlth

14
我发现在处理/主要是/拉丁字符时,一种有效的方法是使用操作符,只有当两个字符串都匹配特定的正则表达式时使用。例如:/^[\w-.\s,]*$/如果两个字符串都匹配表达式,那么速度会更快,最坏的情况似乎比盲目调用localeCompare略慢。
例如在这里: http://jsperf.com/operator-vs-localecompage/11 更新:看起来Intl.Collator目前是性能最好的选择: https://jsperf.com/operator-vs-localecompage/22

非常适合我,值得更多的赞!我的数据集99%没有重音符号,所以你的no_locale正则表达式有很大的作用。 - Codemonkey
你能解释一下这个正则表达式的作用吗? - Stijn de Witt
正则表达式检测字符串是否仅包含字母数字字符。 \w 匹配任何字母数字字符,包括下划线。相当于 [A-Za-z0-9_]。LocaleCompare 对这些字符不相关(在大多数情况下?)。 - Jamie Pate
LocaleCompare 对于字母数字字符并不无关紧要,因为常规比较会将所有大写字符排在小写字符前面。您的 jsperf 测试在调用 localeCompare 之前调用了 toLowerCase()。这是一个无效的性能测试。使用 localeCompare 时,不应使用 toLowerCase() - gilly3
1
Localecompare比toLowerCase慢了很多个数量级,因此后者基本上不重要。我最近重新进行了基准测试,而Intl.Collator在这些天中击败了更快的正则表达式快捷方式版本。https://jsperf.com/operator-vs-localecompage/22 - Jamie Pate

6
没有看到你正在排序的数据,因此很难确定最快的排序方法。不过jsperf有很多好的测试来展示各种排序类型之间的性能差异:http://jsperf.com/javascript-sort/45http://jsperf.com/sort-algorithms/31。 然而,以上测试没有考虑本地化字符串,我认为排序本地化字符串并没有简单的方法,使用localeCompare可能是最佳解决方案。 查看mozilla参考文献说明: “在比较大量的字符串(例如排序大型数组)时,最好创建一个Intl.Collator对象,并使用其compare属性提供的函数。” https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare 但是,在访问Intl.Collator参考文献时,显示它不支持firefox/safari https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator 你可以尝试使用localCompare上的一些选项来加速性能。但是,我刚刚进行了一个快速测试,改变敏感度级别似乎不能提高性能。
list.sort(function(a, b) {
  return a.localeCompare(b, {sensitivity:'base'});
});

http://jsperf.com/sort-locale-strings


1
最好创建一个Intl.Collator对象并使用其compare属性提供的函数 - 绝对同意。我进行了一些测量,是的,使用1000行时,比较速度要快得多,为16毫秒,而使用localCompare则需要25秒。 - Serge

2

尝试分为两步进行排序:

  1. 使用运算符:正如您所说,它将快400倍
  2. 然后使用 localCompare():这样需要比较的数量会更少,因为数组大部分已经排序好了。

注意:我认为大多数情况下,localCompare() 将至少与一个非英语字符串一起调用。因此,使用2个英语字符串调用 localCompare() 的次数应该会大大减少。

以下是代码:

myArray.sort(function(a, b) {
  return (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0));
});

myArray.sort(function(a, b) {
  return a.name.localeCompare(b.name);
});

这个解决方案的优点是简短易用。如果数组主要包含英文字符串,它将非常有效。你拥有的非英文字符串越多,第一个排序就越不实用。但由于它很容易添加到你的脚本中,所以也很容易看出这种方法是否值得尝试。
现在,如果我是你,我还会使用Intl.Collator,因为据说当你需要进行多次比较时,它比localCompare()快得多。

2
并非所有的排序算法都能够利用已经几乎排好序的数组(有趣的是,对于一个非常幼稚的快速排序算法来说这是一场灾难)。不知道Javascript中使用的算法是否可以。 - maaartinus

-3
我不知道你还在寻找解决这个问题的方法。
// Defaulted to ascending
// 1 asc | -1 desc
var direction = 1; 
myArray.sort(function (a, b) {
  return a.name.localeCompare(b.name) === 1 ? direction : -1 * direction;
});

我在你的代码中添加了一个 === 1 检查,这提高了性能 400 倍,这意味着两者具有可比性能数字。

使用 localeCompare arr 大小的性能数字:3200 10 次重复中平均花费时间:60 毫秒

使用 > 方法的性能数字。平均花费时间为 55 毫秒


我不确定这如何解决问题。你可以用你的发现做一个 jsperf 吗? ===1 如何将性能提高 400 倍? - Jamie Pate
5
抱歉,但你的解决方案是错误的:localeCompare()可能返回不同于-1、0或1的值。请查看文档。此外,我非常怀疑添加乘法比没有乘法更快。您应该创建两个比较器:一个升序,一个降序。JIT将能够更好地将它们内联。 - jlgrall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接