更新/简述;
事实证明,这是在幕后使得ajv成为最快的JSON验证器库的方法。
此外,有人将我的想法推向了一个新的高度,并将其用于加速“对对象属性求和”,跨浏览器谱系提高了100倍以上-在这里找到他的jsperf:
粉色条表示他的“预编译求和”方法,它只留下了所有其他方法和操作。
这个技巧是什么?
使用预编译求和方法,他使用了我的代码自动生成了这段代码:
var x = 0;
x += o.a;
x += o.b;
x += o.c;
// ...
这比这个要快得多:
var x = 0;
for (var key in o) {
x += o[key];
}
...特别是当我们访问属性的顺序(a
, b
, c
)与o
的隐藏类中的顺序匹配时,性能会更好。
下面是详细解释:
更快的对象属性循环
首先要说的是,for ... in
循环是可以的,只有在涉及大量CPU和RAM使用的性能关键代码时才需要考虑这个问题。通常情况下,还有更重要的事情需要花时间处理。但是,如果你是一个性能狂热者,你可能会对这个近乎完美的替代方案感兴趣:
Javascript对象
通常,JS对象有两种用法:
- "字典",也称为 "关联数组",是具有不同属性集的通用容器,由字符串键索引。
- "常量类型的对象"(其中所谓的隐藏类始终相同)有固定顺序的一组固定属性。是的!虽然标准没有保证任何顺序,但现代VM实现都有一个(隐藏的)顺序来加速各种操作。在我们稍后探讨中始终维护这个顺序非常重要。
使用"常量类型的对象"而不是"字典类型"通常会更快,因为优化程序了解这些对象的结构。如果你想知道如何实现这个,你可能会想看看Vyacheslav Egorov的博客,这篇博客介绍了V8以及其他Javascript运行时是如何处理对象的。Vyacheslav在这篇博客文章中解释了Javascript的对象属性查找实现。
循环遍历对象属性
默认的for ... in
循环无疑是迭代对象所有属性的好选择。然而,for ... in
可能会将您的对象视为具有字符串键的字典,即使它具有隐藏类型。在这种情况下,每次迭代都会有一个字典查找的开销,这通常实现为哈希表查找。在许多情况下,优化程序足够聪明,避免了这个问题,并且性能与属性名称的常量命名相当,但它并不保证设备可用。经常出现的情况是,优化程序无法帮助你,你的循环将比应该慢得多。最糟糕的是,有时候这是不可避免的,特别是当你的循环变得更加复杂时。优化器并不那么聪明(至少现在还不聪明!)。下面的伪代码描述了
for each key in o: // key is a string!
var value = o._hiddenDictionary.lookup(key); // this is the overhead
doSomethingWith(key, value);
一个未展开、未优化的for ... in
循环,循环遍历一个具有三个特定顺序属性 ['a', 'b', 'c'] 的对象,看起来像这样:
var value = o._hiddenDictionary.lookup('a');
doSomethingWith('a', value);
var value = o._hiddenDictionary.lookup('b');
doSomethingWith('b', value);
var value = o._hiddenDictionary.lookup('c');
doSomethingWith('c', value);
假设你无法优化doSomethingWith
,根据阿姆达尔定律,只有在以下条件都成立时才能获得更高的性能:
doSomethingWith
比字典查找的开销要快得多。
- 你能够真正消除字典查找开销。
我们确实可以使用我所谓的预编译的迭代器来消除这个查找。它是一个专门的函数,用于迭代具有固定类型的所有对象,即具有一组固定顺序属性的类型,并对所有对象执行特定操作。 迭代器通过属性的正确名称显式调用回调(让我们称之为doSomethingWith
)。因此,运行时始终可以利用类型的隐藏类,而无需依赖优化器的承诺。以下伪代码描述了如何针对任何具有三种按照给定顺序排列的属性['a', 'b', 'c']
的对象使用预编译的迭代器:
doSomethingWith('a', o.a)
doSomethingWith('b', o.b)
doSomethingWith('c', o.c)
没有额外开销。我们不需要查找任何东西。编译器已经可以使用隐藏的类型信息轻松计算出每个属性的确切内存地址,甚至使用最缓存友好的迭代顺序。这也是使用 for ... in
和完美的优化器得到的最快代码。
性能测试
这个jsperf 显示预编译的迭代器方法比标准的for ... in
循环要快得多。请注意,加速在很大程度上取决于对象的创建方式和循环的复杂性。由于此测试只有非常简单的循环,因此有时可能观察不到太多加速。但是,在我的一些测试中,我能够看到预编译的迭代器的25倍加速;或者更确切地说, for ... in
循环的显着减速,因为优化器无法摆脱字符串查找。
随着更多测试结果的发布,我们可以对不同的优化器实现得出一些初步结论:
- 预编译的迭代器通常表现得更好,即使在非常简单的循环中也是如此。
- 在IE中,两种方法显示了最小的差异。感谢微软为编写一个不错的迭代优化器(至少对于这个特定问题)!
- 在Firefox中,
for ... in
是远远最慢的。迭代优化器在那里表现不佳。
然而,测试具有非常简单的循环体。我仍在寻找一个测试用例,在其中优化器无法实现跨所有(或几乎所有)浏览器的常数索引。任何建议都非常受欢迎!
代码
JSFiddle在此处。
以下的 compileIterator
函数为任何类型的(简单)对象预编译迭代器(暂时忽略嵌套属性)。迭代器需要一些额外的信息,表示它应该遍历的所有对象的确切类型。这样的类型信息通常可以表示为字符串属性名称的数组,以确切的顺序, declareType
函数采用该数组来创建一个方便的类型对象。如果您想看到更完整的示例,请参考 jsperf 条目。
var compileIterator = function(typeProperties) {
var iteratorFunStr = '(function(obj, cb) {\n';
for (var i = 0; i < typeProperties.length; ++i) {
iteratorFunStr += 'cb(\'' + typeProperties[i] + '\', obj.' + typeProperties[i] + ');\n';
};
iteratorFunStr += '})';
return eval(iteratorFunStr);
};
var declareType = function(propertyNamesInOrder) {
var self = {
propertyNamesInOrder: propertyNamesInOrder,
forEach: compileIterator(propertyNamesInOrder),
construct: function(initialValues) {
var o = {};
propertyNamesInOrder.forEach((name) => o[name] = initialValues[name]);
return o;
}
};
return self;
};
这是我们如何使用它:
// ########################################################################
// Declare any amount of types (once per application run)
// ########################################################################
var MyType = declareType(['a', 'b', 'c']);
// ########################################################################
// Run-time stuff (we might do these things again and again during run-time)
// ########################################################################
// Object `o` (if not overtly tempered with) will always have the same hidden class,
// thereby making life for the optimizer easier:
var o = MyType.construct({a: 1, b: 5, c: 123});
// Sum over all properties of `o`
var x = 0;
MyType.forEach(o, function(key, value) {
// console.log([key, value]);
x += value;
});
console.log(x);
JSFiddle此处。
for/in
循环不会枚举继承的属性/方法。 - Josh Stodola