问题
我的基准测试有问题吗? Immutable.js find()如何比array.find()慢8倍?
好吧,不完全公平,因为我在Immutable.List中使用了Immutable.Map。但对我来说,这是一个现实世界的例子。如果我使用Immutable.js来保护不变性并在某些方面获得性能(结构共享起作用)。仅在对象的根处使用Immutable.js是没有意义的。
以下基准实际上来自another question(我也是)。我对结果感到非常惊讶,我不得不单独发布以使其顺利进行。我在基准测试中做错了什么,或者性能差异真的很大吗?
背景
我的应用中的部分数据可被视为应用元数据。原始数据位于服务器的数据库中。不会经常更新元数据。该应用程序将在启动时检查更新的元数据。
我到处都在使用Immutable.js,但我会回到简单的js来获取元数据。这类数据不需要花哨的结构共享。
测试是按集合中的键查找值
收集10件物品
查找一百万次的值
Mac mini core i7 2.6
结果:
带有强制密钥的普通JS对象: 8 ms
使用find()的纯JS数组: 127 ms
使用数字键的Immutable.Map: 185 ms
使用find()的Immutable.List: 972 ms !! 我感到困惑
当我使用React Native时,如果我想达到60 fps,我总是要注意16 ms的限制。基准值似乎不是线性的。仅使用100次查找运行测试需要1 ms的Map和2 ms的List。那太贵了。
let Immutable = require('immutable');
let mapTest = Immutable.Map()
.set(1, Immutable.Map({value: 'one'}))
.set(2, Immutable.Map({value: 'two'}))
.set(3, Immutable.Map({value: 'three'}))
.set(4, Immutable.Map({value: 'four'}))
.set(5, Immutable.Map({value: 'five'}))
.set(6, Immutable.Map({value: 'six'}))
.set(7, Immutable.Map({value: 'seven'}))
.set(8, Immutable.Map({value: 'eight'}))
.set(9, Immutable.Map({value: 'nine'}))
.set(10, Immutable.Map({value: 'ten'}));
let listTest = Immutable.fromJS([
{key: 1, value: 'one'},
{key: 2, value: 'two'},
{key: 3, value: 'three'},
{key: 4, value: 'four'},
{key: 5, value: 'five'},
{key: 6, value: 'six'},
{key: 7, value: 'seven'},
{key: 8, value: 'eight'},
{key: 9, value: 'nine'},
{key: 10, value: 'ten'}
])
let objTest = {
1: {value: 'one'},
2: {value: 'two'},
3: {value: 'three'},
4: {value: 'four'},
5: {value: 'five'},
6: {value: 'six'},
7: {value: 'seven'},
8: {value: 'eight'},
9: {value: 'nine'},
10: {value: 'ten'}
};
let arrayTest = [
{key: 1, value: 'one'},
{key: 2, value: 'two'},
{key: 3, value: 'three'},
{key: 4, value: 'four'},
{key: 5, value: 'five'},
{key: 6, value: 'six'},
{key: 7, value: 'seven'},
{key: 8, value: 'eight'},
{key: 9, value: 'nine'},
{key: 10, value: 'ten'}
];
const runs = 1e6;
let i;
let key;
let hrStart;
console.log(' ')
console.log('mapTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
let result = mapTest.getIn([key, 'value'] )
key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);
console.log(' ')
console.log('listTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
let result = listTest
.find(item => item.get('key') === key)
.get('value');
key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);
console.log(' ')
console.log('arrayTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
let result = arrayTest
.find(item => item.key === key)
.value
key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);
console.log(' ')
console.log('objTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
let result = objTest[key].value
key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);
答案 0 :(得分:20)
简短的回答是,与原生JS数组相比,Immutable.js使用的数据结构的表示需要大量额外的开销来迭代List的元素。
您的基准测试很好,但我们可以通过摆脱嵌套地图来简化问题;您是否正确考虑性能以解决实际问题,但它有助于理解性能差异以尽可能简化问题。在基准测试中,它通常也很有用,可以考虑性能如何随不同的输入大小而变化。例如,可能在Immutable.js中,List.prototype.find
的实现方式使得初始调用和设置需要一段时间,但后续迭代通过List执行类似于本机JS数组;在这种情况下,对于长输入长度,本机JS数组和Immutable.js列表之间的性能差异会减小(事实证明并非如此)。
让我们为本机JS数组Array.prototype.ourFind
创建自己的查找函数,与原生Array.prototype.find
进行比较,以确定差异是否部分归因于JS函数的性能本身与内置于实现的函数的性能。
Array.prototype.ourFind = function(predicate) {
for (let i = 0; i < this.length; i++) {
if (predicate(this[i])) return this[i];
}
}
function arrayRange(len) {
return new Array(len).fill(null).map((_, i) => i);
}
function immutListRange(len) {
return Immutable.fromJS(arrayRange(len));
}
function timeFind(coll, find, iters) {
let startTime = performance.now();
for (let i = 0; i < iters; i++) {
let searchVal = i % coll.length,
result = find.call(coll, item => item === searchVal);
}
return Math.floor(performance.now() - startTime);
}
const MIN_LEN = 10,
MAX_LEN = 1e4,
ITERS = 1e5;
console.log('\t\tArray.find\tArray.ourFind\tList.find');
for (let len = MIN_LEN; len <= MAX_LEN; len *= 10) {
console.log(`${len}\t\t\t` +
`${timeFind(arrayRange(len), Array.prototype.find, ITERS)}\t\t\t` +
`${timeFind(arrayRange(len), Array.prototype.ourFind, ITERS)}\t\t\t` +
`${timeFind(immutListRange(len), Immutable.List.prototype.find, ITERS)}`)
}
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
&#13;
在Chrome中,我得到:
Length . Array.find Array.ourFind List.find
10 28 13 96
100 60 44 342
1000 549 342 3016
10000 5533 3142 36423
我在Firefox和Safari中获得了大致相似的结果。需要注意几点:
List.find
与Array.find
之间的差异不仅仅是由于本机(即内置解释器)实现与JS实现之间的差异,因为Array.ourFind
的JS实现至少执行以及Array.find
。Immutable.List.find
比Array.find
慢约6倍,与您的基准测试结果一致。要理解为什么Immutable.List.find
速度慢得多,首先要考虑Immutable.List
如何表示列表内容。
快速执行此操作的方法是生成Immutable.List
并在控制台中检查它:
console.log(immutListRange(1000)); // immutListRange defined above
基本上看起来Immutable.List
表示内容为树,分支因子为32。
现在考虑对以这种方式表示的数据运行查找操作需要什么。您必须从根节点开始,并将树遍历到第一个叶节点(其中包含具有实际数据的Array),并遍历叶的内容;如果找不到该元素,则必须转到下一个叶节点并搜索该数组,依此类推。它比仅仅搜索单个数组更复杂,并且需要执行开销。
欣赏Immutable.List.find
所做工作的一个好方法是在您选择的调试器中设置一个断点并逐步完成操作。您将看到Immutable.List.Find
的操作并不像仅循环单个数组那样简单。
Immutable.js中数据的树表示可能会加速其他操作,但会导致某些函数(例如find)的性能损失。
作为旁注,我不认为在大多数情况下,使用不可变数据结构的选择是由性能考虑因素驱动的。可能存在一些情况,其中不可变数据结构比可变数据结构表现更好(当然,不可变数据结构使并行计算不那么复杂,这可以显着提高性能),但是在相反情况下会有很多情况。相反,在大多数情况下,不变性的选择是由设计考虑因素驱动的 - 即。使用不可变数据结构会强制程序设计更加强大,从长远来看,可以提高开发人员的工作效率。
答案 1 :(得分:11)
JS引擎非常善于优化&#39; hot&#39;操作 - 重复很多且尽可能简单的操作(例如TurboFan in V8)。简单的JS对象和数组函数始终将击败像Immutable.js这样的库,其中List
调用Collection
调用Seq
调用Operations
(等等),特别是当动作重复多次时。
Immutable.js似乎是为了方便使用而设计的,并且避免了可变JS集合的大量讨厌,而不是纯粹的性能。
如果您有一百万件事,请使用低级JS对象或数组(或Web组件,如果性能至关重要)。如果你有一千件事情并且需要以确定不丢帧,那么普通JS仍然是可行的方法。这些是特殊情况 - 对于大多数用例而言,Immutable.js的便利性值得降低速度。
答案 2 :(得分:1)
基准测试并未考虑Immutable必须提供的所有数据类型。不可变实际上具有一些功能,而普通对象/数组则没有:OrderedSet和OrderedMap既具有索引数组/列表又具有基于键的结构(如对象/地图)的优点。
下面是@Keith测试良好的改编版本,它显示出我们实际上可以比Array.find更快,尤其是对于大型数据集。
当然,这也要付出一些代价:
请注意,OrderedSet比无序Set昂贵,并且可能消耗更多内存。 OrderedSet#add已摊销O(log32 N),但不稳定。
function arrayFind(coll, searchVal) {
return coll.find(item => item === searchVal);
}
function immutableSetGet(coll, searchVal) {
return coll.get(searchVal);
}
function arrayRange(len) {
return new Array(len).fill(null).map((_, i) => i);
}
function immutOrderedSetRange(len) {
return Immutable.OrderedSet(arrayRange(len));
}
function timeFind(what, coll, find, iters) {
let startTime = performance.now();
let size = coll.length || coll.size;
for (let i = 0; i < iters; i++) {
let searchVal = i % size,
result = find(coll, searchVal);
}
return Math.floor(performance.now() - startTime);
}
const MIN_LEN = 100,
MAX_LEN = 1e4,
ITERS = 50000;
console.log('\t\t\tArray.find\tOrderedSet.find');
for (let len = MIN_LEN; len <= MAX_LEN; len *= 10) {
console.log(`${len}\t\t\t` +
`${timeFind('find', arrayRange(len), arrayFind, ITERS)}\t\t` +
`${timeFind('set', immutOrderedSetRange(len), immutableSetGet, ITERS)}`)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>