I'm writing a JS object that needs to perform really basic key-value caching on string:function pairs. The class runs on the client and caches partially-compiled templates for rendering part of the page, so it may have anywhere from 20-200 items.
Before actually writing the class, I figured it would be a good idea to see what the fastest cache retrieval method was. The options that came to mind were:
1. Basic property access:
if (x[k] !== undefined) {
v = x[k];
}
2. Key Check (Own):
if (x.hasOwnProperty(k)) {
v = x[k];
}
3. Key Check (General):
if (k in x) {
v = x[k];
}
I assumed that 3 would be fastest (checks to see if the property exists but doesn't retrieve it or worry about where it exists) and 1 would be slowest (actually gets the property, even if it doesn't do anything).
Putting all of these into jsPerf yielded some very strange results. In both Chrome (and Chromium) and IE, #1 is about twice as fast. In Firefox, #3 has a minor edge, but performance is similar between all three. It didn't matter if I was running in a VM or not, and didn't change a lot between versions.
I'm having trouble explaining these results. It might be that #1 notices that nothing will happen to the data and so just checks for the key internally, but why is it faster than #3? Why does #3 not get the same optimization?
What is causing these results? Is there some JIT optimization I might be hitting that skews the data?
More importantly, why is this so drastically different between browsers, with all options being roughly equal in FF?
hasOwnProperty() is faster. I realize there are other ways to make the loops faster, like storing lengths in variables.
So what's the difference between the two? The key difference is that in will return true for inherited properties, whereas hasOwnProperty() will return false for inherited properties.
The hasOwnProperty() method returns a boolean indicating whether the object has the specified property as its own property (as opposed to inheriting it).
The hasOwnProperty() method returns true if the property is directly present in the object (not in its prototype chain). If an object is an Array, then the hasOwnProperty() method can check if an index is available (not empty) in the array.
The secret behind x[k]
performance on Chrome (V8) is in this chunk of assembly from ic-ia32.cc
. In short: V8 maintains a global cache that maps a pair of (map, name)
to an index
specifying location of the property. Map is an internal name used in V8 for hidden classes other JS engines call them differently (shapes in SpiderMonkey and structures in JavaScriptCore). This cache is populated only for own properties of fast mode objects. Fast mode is the representation of an object that does not use dictionary to store properties, but instead is more like a C-structure with properties occupying fixed offsets.
As you can see once the cache is populated the fist time your loop is executed, it will always be hit on the subsequent repetitions, meaning that the property lookup will always be handled inside the generated code and will never enter runtime because all properties benchmark is looking up actually exist on the object. If you profile the code you will see the following line:
256 31.8% 31.8% KeyedLoadIC: A keyed load IC from the snapshot
and dumping native code counters would show this (actual number depends on the number of iterations you repeat the benchmark):
| c:V8.KeyedLoadGenericLookupCache | 41999967 |
which illustrates that cache is indeed being hit.
Now V8 does not actually use the same cache for either x.hasOwnProperty(k)
or k in x
, in fact it does not use any cache and always end up calling runtime, e.g. in the profile for hasOwnProperty
case you will see a lot of C++ methods:
339 17.0% 17.0% _ZN2v88internal8JSObject28LocalLookupRealNamedPropertyEPNS0_4NameEPNS0_12LookupResultE.constprop.635 254 12.7% 12.7% v8::internal::Runtime_HasLocalProperty(int, v8::internal::Object**, v8::internal::Isolate*) 156 7.8% 7.8% v8::internal::JSObject::HasRealNamedProperty(v8::internal::Handle<v8::internal::JSObject>, v8::internal::Handle<v8::internal::Name>) 134 6.7% 6.7% v8::internal::Runtime_IsJSProxy(int, v8::internal::Object**, v8::internal::Isolate*) 71 3.6% 3.6% int v8::internal::Search<(v8::internal::SearchMode)1, v8::internal::DescriptorArray>(v8::internal::DescriptorArray*, v8::internal::Name*, int)
and the main problem here is not even that these are C++ methods and not handwritten assembly (like KeyedLoadIC stub) but that these methods are performing the same lookup again and again without caching the outcome.
Now the implementations can be wildly different between engines, so unfortunately I can't give complete explanation of what happens on other engines, but my guess would be that any engine that shows faster x[k]
performance is employing similar cache (or represents x
as a dictionary, which would also allow fast probing in the generated code) and any engine that shows equivalent performance between cases either does not use any caching or employs that same cache for all three operations (which would make perfect sense).
If V8 probed the same cache before going to runtime for hasOwnProperty
and in
then on your benchmark you would have seen equivalent performance between cases.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With