更新/TLDR;
事实证明,这种方法使ajv 成为最快的 JSON 验证器库。
另外 - 有人把我的想法提升到了一个新的水平,并用它在整个浏览器范围内将“对象属性求和”速度提高了 100 倍以上 - find his jsperf here:
粉色条代表他的“预编译总和”方法,它将所有其他方法和操作都抛在了脑后。
有什么诀窍?
使用预编译求和的方式,他用我的代码自动生成了这段代码:
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 的hidden class 中的顺序相匹配。
详细解释如下:
更快的对象属性循环
首先让我说,for ... in 循环很好,您只想在具有大量 CPU 和 RAM 使用的性能关键代码中考虑这一点。通常,您应该花时间在更重要的事情上。但是,如果您是性能狂,您可能会对这种近乎完美的替代方案感兴趣:
Javascript 对象
一般来说,JS 对象有两种用例:
- “字典”,也称为“关联数组”,是具有不同属性集的通用容器,由字符串键索引。
- “常量类型的对象”(所谓的hidden class 始终相同)具有一组固定顺序的固定属性。是的! - 虽然标准不保证任何顺序,但现代 VM 实现都有一个(隐藏的)顺序,以加快速度。正如我们稍后将探讨的那样,始终保持这种秩序至关重要。
使用“常量类型的对象”而不是“字典类型”通常要快得多,因为优化器了解这些对象的结构。如果您对如何实现这一点感到好奇,您可能想查看Vyacheslav Egorov's blog,它揭示了V8 以及其他Javascript 运行时如何处理对象的大量信息。 Vyacheslav explains Javascript's object property look-up implementation in this blog entry.
循环遍历对象的属性
默认的for ... in 肯定是迭代对象所有属性的好选择。但是,for ... in 可能会将您的对象视为带有字符串键的字典,即使它具有隐藏类型。在这种情况下,在每次迭代中,您都有字典查找的开销,这通常实现为hashtable lookup。在许多情况下,优化器足够聪明,可以避免这种情况,并且性能与属性的常量命名相当,但根本无法保证。很多时候,优化器不能帮助你,你的循环运行速度会比它应该运行的慢很多。最糟糕的是,有时这是不可避免的,尤其是当您的循环变得更加复杂时。优化器只是没有那么聪明(还没有!)。以下伪代码描述了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,Amdahl's law 告诉我们,当且仅当:
-
doSomethingWith 已经非常快了(与字典查找的成本相比)并且
- 您实际上可以摆脱字典查找开销。
我们确实可以使用我所说的预编译的迭代器来摆脱这种查找,这是一个迭代固定类型的所有对象的专用函数,即具有固定集合的类型固定顺序的属性,并对所有这些属性执行特定的操作。该迭代器通过其正确名称显式调用每个属性的回调(我们称之为doSomethingWith)。结果,运行时总是可以使用类型的hidden class,而不必依赖优化器的承诺。以下伪代码描述了预编译迭代器如何按给定顺序对具有三个属性['a', 'b', 'c'] 的任何对象起作用:
doSomethingWith('a', o.a)
doSomethingWith('b', o.b)
doSomethingWith('c', o.c)
没有开销。我们不需要查任何东西。编译器已经可以使用隐藏的类型信息轻松计算每个属性的确切内存地址,它甚至使用对缓存最友好的迭代顺序。这也是(非常非常接近)使用for...in 和完美优化器可以获得的最快代码。
性能测试
This jsperf 表明预编译的迭代器方法比标准的for ... in 循环快很多。请注意,加速很大程度上取决于对象的创建方式和循环的复杂性。由于此测试只有非常简单的循环,因此您有时可能不会观察到太多的加速。然而,在我自己的一些测试中,我能够看到预编译迭代器的 25 倍加速;或者更确切地说是for ... in 循环的显着减慢,因为优化器无法摆脱字符串查找。
通过more tests coming in,我们可以对不同的优化器实现得出一些初步结论:
- 预编译的迭代器通常性能要好得多,即使在非常简单的循环中也是如此。
- 在 IE 中,这两种方法的差异最小。 Bravo Microsoft 编写了一个不错的迭代优化器(至少对于这个特定问题)!
- 在 Firefox 中,
for ... in 是最慢的。那里的迭代优化器做得不好。
但是,测试有一个非常简单的循环体。我仍在寻找一个测试用例,其中优化器永远无法在所有(或几乎所有)浏览器中实现恒定索引。非常欢迎任何建议!
代码
JSFiddle here.
以下compileIterator 函数为任何类型的(简单)对象预编译迭代器(暂时不考虑嵌套属性)。迭代器需要一些额外的信息,代表它应该迭代的所有对象的确切类型。这样的类型信息通常可以表示为字符串属性名称的数组,具有精确的顺序,declareType 函数使用它来创建方便的类型对象。如果想看更完整的例子,请参考jsperf entry。
//
// Fast object iterators in JavaScript.
//
// ########################################################################
// Type Utilities (define once, then re-use for the life-time of our application)
// ########################################################################
/**
* Compiles and returns the "pre-compiled iterator" for any type of given properties.
*/
var compileIterator = function(typeProperties) {
// pre-compile constant iteration over object properties
var iteratorFunStr = '(function(obj, cb) {\n';
for (var i = 0; i < typeProperties.length; ++i) {
// call callback on i'th property, passing key and value
iteratorFunStr += 'cb(\'' + typeProperties[i] + '\', obj.' + typeProperties[i] + ');\n';
};
iteratorFunStr += '})';
// actually compile and return the function
return eval(iteratorFunStr);
};
// Construct type-information and iterator for a performance-critical type, from an array of property names
var declareType = function(propertyNamesInOrder) {
var self = {
// "type description": listing all properties, in specific order
propertyNamesInOrder: propertyNamesInOrder,
// compile iterator function for this specific type
forEach: compileIterator(propertyNamesInOrder),
// create new object with given properties of given order, and matching initial values
construct: function(initialValues) {
//var o = { _type: self }; // also store type information?
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 here.