06.04.10

More changes coming to SpiderMonkey: the magical __count__ property is being removed

Tags: , , , , , — Jeff @ 17:17

Meet __count__

SpiderMonkey for some time has included a little-known, little-publicized, and little-used property named __count__ on all objects. This property stored the count of the number of enumerable properties directly on the object. For example,

assertEqual({ 1: 1 }.__count__, 1);
assertEqual([].__count__, 0);
assertEqual([1].__count__, 1);
assertEqual([1, /* hole */, 2, 3].__count__, 3);

It’s sort of a convenient way to check property counts, avoiding a verbose for (var p in o) loop. For example, you could use it to determine how many mappings you had in a hash.

Unfortunately, __count__ has a number of problems.

What’s wrong with __count__

First, and most notably for web developers, __count__ is non-standard. To the best of my knowledge, no other JavaScript engine supports it. Developers must write scripts under the assumption it doesn’t exist, so it provides no brevity bonus. (I recognize extensions and Mozilla-based applications are a special case. For the further reasons below, it’s still worth removing even if some code could assume its existence.)

Second, the special __count__ property contributes to the problem that you can’t actually use an object as a string-value hash. The reason is that you have to be careful that your string-valued keys don’t conflict with special properties, because special properties can’t be overwritten with a custom property. This breaks the nicest feature of using objects for hashes: you can’t just use normal [] property access to set and retrieve mappings. If someone inserts a mapping of, say, "__count__""special", the association with "special" won’t be preserved; obj.__count__ = 5 is actually a no-op. (NB: Even ignoring __count__ other special properties prevent this from working, but we’re slowly working toward getting rid of them. [Some much more slowly than others, I hasten to note! You don't need to worry about __proto__ being removed any time in the near future, although you should use Object.getPrototypeOf(obj) with a compatibility shim to determine obj's prototype in new code.]) A further wrinkle is that __count__ is implemented in such a way that an object literal with "__count__" as a named property functions differently from an object to which the property is later added by assignment:

var o = { __count__: 17 };
assertEqual(o.__count__, 17);

// ...but...
var o2 = {};
o2.__count__ = 17;
assertEqual(o2.__count__, 17); // fails: __count__ is 0

Third, in supporting __count__ we’ve incurred special-case deoptimization code in SpiderMonkey’s script-to-bytecode compiler. This extra complication, for a feature not often used, does nothing for code readability, complexity, or quality.

Fourth, __count__ doesn’t work the way you might think it works: it’s not uniformly fast for all objects. Property access generally has a syntactic assumption of constant-time speediness. This assumption isn’t valid in languages with getters and setters, but since it’s usually the case, it’s not a horribly inaccurate one. Thus, one might expect that evaluating obj.__count__ is an uncomplicated O(1) operation which doesn’t allocate memory and just looks up the size of an idealized hash table. It might be possible to make that true, but in fact it never has been true: generally, computing __count__ is O(n) in the number of properties on the object. Further, because __count__ reuses the same enumeration mechanism as for loops, it usually requires a memory allocation, which can be slow. __count__ has no asymptotic advantage over manual enumeration of the object’s properties.

In sum, __count__ has problems that mean it doesn’t give you much more than a for (var p in o) loop would. If that loop were placed in a function, it would be almost identical in code size to use of the property — and it would have the advantage of being completely cross-browser.

__count__ is being removed

We have removed support for __count__ from SpiderMonkey. As a consequence __count__ will also be removed from the next version of Firefox based on trunk Mozilla code. (And, of course, future versions of other Mozilla-based products like SeaMonkey will pick the change up when they produce releases based on trunk Mozilla code.) For the above reasons __count__ doesn’t make much sense to keep around, and it imposes real development costs. You should have no difficulty updating your code to implement alternative functionality to __count__. Here’s one example of how you might do this:

function count(o)
{
  var n = 0;
  for (var p in o)
    n += Object.prototype.hasOwnProperty.call(o, p);
  return n;
}

assertEqual(count({ 1: 1 }), 1);
assertEqual(count([]), 0);
assertEqual(count([1]), 1);
assertEqual(count([1, /* hole */, 2, 3]), 3);

If you use __count__ and need to test changes to remove that use, you can experiment with a version of Firefox with support for __count__ removed by downloading a nightly from nightly.mozilla.org. (Don’t forget to use the profile manager if you want to keep the settings you use with your primary Firefox installation pristine.)

6 Comments »

  1. A brief followup note on __count__‘s algorithmic complexity: in the past it hasn’t even been obvious to me that __count__ wasn’t a constant-time property, as a comment in netwerk/test/httpserver/httpd.js shows.

    Comment by Jeff — 06.04.10 @ 21:50

  2. Good thing I didn’t know about that property, I might have been tempted to use it :)

    Looks like there are always new things to learn about SpiderMonkey. Just recently I found out about sharp variables – might be a nice thing to use as well but I am afraid that nobody will understand my code then. Not to mention that this might go away as well at some point since nobody really uses them (probably the instant that something more standardized comes up).

    Comment by Wladimir Palant — 07.04.10 @ 00:14

  3. Well reasoned. :-) Sharp variables are another thing I want to kill (and eventually, yes, we will kill them) — they interfere with decompilation and parsing far more than __count__ does, and have produced far more security-sensitive bugs — but that slog is slower and harder because alternative options aren’t as featureful. Frankly, I say kill them anyway even if there isn’t a perfect replacement, but other constituencies fight for the other option, so it’s a slog.

    In general, having seen how most of our extensions to ECMAScript have fared in practice, I’m almost always wary of adding new stuff that goes beyond library updates these days. The array extras are the only major feature I can think of in recent times that has gone well in practice, without producing a mass of extra work in fixing security bugs and integration errors (whole-system bugs through quadratic [and worse] interaction of features). The /y flag to regular expressions is a minor feature which has also been mostly uneventful. Beyond that? Script objects, watch points, E4X, sharp variables, custom iteration hooks making the iteration protocol harder to speed up, function decompilation (rather than taking the far safer, if aesthetically less pleasing, option of just saving function source directly), old-style getter and setter syntax (this is not the excellent, de facto and recently de jure ES5 getter/setter syntax, it’s something quite different), generators, array comprehensions, generator expressions, these things have all been sources of many bugs with security implications. Most of them don’t even see much use, either, compared to bog-standard ES3 (or lately, ES5). I’d also include writable __proto__ in there, although its uses are far more ubiquitous and sensible than any of these other ideas (but we have a solution for even that which should let us remove that source of problems). Heck, even getters and setters have been a bit rocky, especially in their interactions with all these other features (although as a fundamental part of the DOM they are beyond mandatory for any real-world JS engine). And, of course, most of these things conflict directly with evolution of ECMAScript in ES5.

    I don’t have anything against imagining and implementing new functionality, but I feel there’s a certain wild wild west-ness to what we’ve done so far. With more of the innovation happening in the context of a better-defined standard and object protocol in ES5, and with there being more effort to work through the wrinkles of the semantics rather than trying to apply semantics to wrinkles (the E4X-from-Rhino mis-example), I have more optimism about upcoming introductions like true hash tables and proxies. More cooks spoil the broth at some point — but what we have isn’t a broth but rather a multi-course dinner that requires small contributions from many people to come together correctly and function as a single well-designed whole.

    Comment by Jeff — 07.04.10 @ 02:46

  4. https://bugzilla.mozilla.org/show_bug.cgi?id=551529 for those interested.

    Comment by Dan — 07.04.10 @ 11:52

  5. I can’t speak to the implementation costs or usage on the web, but getters/setters (however problematic their implementation), let definitions, array comprehensions, and custom iterators, among other ECMAScript extensions, have been godsends for Firefox (core, addon, labs, etc.) developers programming in the large, and we should be careful not to throw out the baby with its admittedly murky bathwater.

    Comment by Myk Melez — 26.04.10 @ 14:20

  6. I doubt you need to worry too much, I don’t have enough buy-in on my side yet. :-( In any case, I’m more cautioning against underexamined syntax change in the future than advocating wholesale breakage of past indiscretions.

    Comment by Jeff — 26.04.10 @ 17:24

RSS feed for comments on this post. TrackBack URI

Leave a comment

HTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>