3

One can expect code similar to this

for(let i = 0 ; i !== N ; ++i ) break

to be of O(1) complexity.

But in case of for(of) or for(in) one can never be sure. Because it strongly depends on the engine's specifics.

So, the following snippet on my Chrome 87, FF 83 and Node.js 14 gives around 0ms (0.01ms) and 150ms for small and big objects, respectively.

const create_obj = n => {
  return Object.fromEntries((function *() {
    for(let i = 0; i!==n ; ++i) yield [i,i]
  })())
}

const check_forin_peformance = obj => {
  let start_time = performance.now()
  for(const key in obj) {
    console.log("first iteration after", performance.now() - start_time, "ms")
    break
  }
}
const small_obj = create_obj(1_000)
const big_obj = create_obj(1_000_000)

console.log("small obj has", Object.keys(small_obj).length, "fields")
check_forin_peformance(small_obj)
console.log("big obj has", Object.keys(big_obj).length, "fields")
check_forin_peformance(big_obj)

Which leads me to believe that it's of O(N) complexity. Despite the fact that there is the break and only one iteration is performed.

In FF for(const x of Object.keys(obj)) gives much better performance. But still looks liner:

let obj = Object.fromEntries((function *(){ for(let i = 0 ; i !== 1_000_000; ++i ) yield [i,i] })())
let start_time = performance.now()
let x = Object.keys(obj).length
console.log(performance.now() - start_time);

obj = Object.fromEntries((function *(){ for(let i = 0 ; i !== 1_000_000; ++i ) yield [i,i] })())
start_time = performance.now()
for(const x of Object.keys(obj)) { console.log("first iteration after", performance.now() - start_time); break; }

obj = Object.fromEntries((function *(){ for(let i = 0 ; i !== 1_000_000; ++i ) yield [i,i] })())
start_time = performance.now()
for(const x in obj) { console.log("first iteration after", performance.now() - start_time); break; }

It's not totally clear, but this is probably due to this section of the spec. And specifically due to these lines

 i. Let `keys` be ? `object`.[[OwnPropertyKeys]]().
ii. For each `key` of `keys` in List order, do
      1. If `Type(key)` is String, then
             a. Append `key` to `remaining`.

So, as there're probably some lists created before the first iteration, O(N) is a good guess. But this is obviously sub-optimal. One can imagine an algorithm to enumerate object's keys with a help of an iterator, so that the first iteration starts after O(1) amount of time. But this is the way it is now.

And I'm wondering: is there anything that can be done? So that if I iterate over n keys (which is much smaller than N) I get O(n) complexity and not O(N). Maybe there is some clever trick I'm not aware about. But it should work with any object not only those I create myself.

And a reference to the place(s) in V8 sources where this all happens will also be appreciated.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
x00
  • 13,643
  • 3
  • 16
  • 40
  • 1
    The operation you're performing is intrinsically *O(n)*. Examining each element in a list of *n* elements is *O(n)*; it's like the basic definition of what "linear time" means. – Pointy Dec 03 '20 at 13:51
  • 4
    @Pointy. There is no reason for it to be `O(N)` - there is `break` in the loop, so it breaks on the **first** iteration. Which should give `O(1)` – x00 Dec 03 '20 at 13:53
  • That's not how worst-case performance analysis works. It's not about a *particular* application of an algorithm. – Pointy Dec 03 '20 at 13:54
  • 1
    @Pointy. Shall we take C++ instead? `for(int i=0; i != -1; ++i) break` how long will take in **any** application of the algorithm? – x00 Dec 03 '20 at 13:57
  • Where are you getting your complexity metrics from? If you unconditionally break a loop then yes, you no longer really have a "loop". – Pointy Dec 03 '20 at 13:59
  • 2
    As a base-line, i'd expect, that internally, the engine prepares an iterable list of all keys, before the loop starts. As the spec guarantees property order, and for other reasons, i wouldn't be surprised, if the internal representation of an object is not suited as-is. There may be optimizations coming in later (e.g. code analysis notices, that it breaks early, and there being a shortcut), but i wouldn't count on them. – ASDFGerte Dec 03 '20 at 14:00
  • @Pointy. Well, the `O(N)` is an estimation, but for `Object.keys()` (or any internal method that will work with `[[VisitedKeys]]` and `[[RemainingKeys]]`) - a good one, I guess. The only other alternative is to suspect it to be even bigger. – x00 Dec 03 '20 at 14:03
  • Weirdly, I get consistently lower runtime for `Object.values()` (7-10ms) than `Object.keys` (30-35ms) using the last snippet. That's in Firefox on Windows 10. Not sure it really means much, however. – VLAZ Dec 03 '20 at 14:04
  • 1
    Well if you *call* `Object.keys()`, then by definition the runtime has to prepare an array. It's probably true that the internal representation of the set of property names is *not* a normal native JavaScript array, and even if it were the runtime would almost certainly make a copy. – Pointy Dec 03 '20 at 14:05
  • @ASDFGerte, yes I have the same suspicions, but I believe it can be optimized. Either way, I'm wondering about if anything can be done about it in _js_ right now. It it possible theoretically or not - another matter entirely. But thanx. – x00 Dec 03 '20 at 14:07
  • @Pointy, all true. If you meant that `Object.keys()` has `O(n)` time, then that means that I worded my question not clear enough. About `Object.keys()` I just said that is has better performance, not better compexity – x00 Dec 03 '20 at 14:10
  • @x00 what is your goal? You want to check for empty objects but...how empty are we talking about? No own keys? `new Date()` will produce an object without own keys, for example. No own keys and no prototype chain? Also doable but there are also symbol keys that are not going to be included in `for...in` or `Object.keys`, for example. – VLAZ Dec 03 '20 at 14:11
  • Just to make sure, your question boils down more or less to "is there a way to iterate an object's keys, with an early break, that doesn't have O(n) on large objects"? In that case, i'd advise using a different/custom data structure. – ASDFGerte Dec 03 '20 at 14:11
  • Also, does this need to be *completely* generic or are you in control of creating the objects? Because if you are, you might be able to cheat using a proxy that will write a symbol property whenever a write to the object happens. If that property was never changed, then the object hasn't had any properties written. – VLAZ Dec 03 '20 at 14:14
  • @VLAZ, I don't have a particular goal right now. But if I will I'd like to know my options. Or if there is none. Any way to enumerate any type of props with a complexity less than `O(n)` - would be great to know. For me and for others as well. – x00 Dec 03 '20 at 14:17
  • @VLAZ. No if I control the objects I can fix this issue right away. So it is about **some** objects. But yes. that one way to do it – x00 Dec 03 '20 at 14:18
  • @ASDFGerte, thanks. As I said in previous comment. Sadly, that's not an option – x00 Dec 03 '20 at 14:19
  • @x00 Then I don't think there is a way. Not one that is generally applicable anyway. You can get the own keys of an object in several way but it's always in the form of an array. At least, I'm not aware of any way to get them as an iterator like, e.g., `Map#keys` does. There are a few shortcuts available but are very tied down with what you're actually working with. The proxy option is one of those but you can also change your data or the data structure or do few other variations. So, no super useful and generic approach, at least as far as I'm aware. – VLAZ Dec 03 '20 at 14:21
  • @VLAZ, Well, yea. Sadly and probably so. But I needed to ask because maybe there is some clever hack I'm not aware of. After all, JS itself - is one big clever hack :) – x00 Dec 03 '20 at 14:26
  • @VLAZ. And about `Object.values()`. It's quiet old and about Chrome, not FF and with opposite results. But it can enlighten you a little bit on the performance of `Object.values() & Co` https://bugs.chromium.org/p/v8/issues/detail?id=6804&q=forin&can=2 And how they are **unrelated** :) – x00 Dec 03 '20 at 14:31
  • 1
    Try using an object that has non-integer keys. You're essentially building an array here, for which `for … in` and `Object.keys` were not optimised. – Bergi Dec 05 '20 at 16:06
  • @Bergi, nice observation! I've tried `yield ["a"+i,i]` and, sadly, speed dropped about 1.5 times, and if i try it with 10_000_000 keys Chrome (at least the console) freezes in this case. Or did you mean something else? By the way, with pure arrays, if I do `Array(N).fill(0)` liner complexity stands. – x00 Dec 05 '20 at 17:46
  • @x00 Yes, something like that. And of course, you never should build such huge objects in real-world code, you'd use a `Map` instead - I'm certain an iterator-based `for (const k of map.keys()) break;` loop will be in `O(1)`. – Bergi Dec 05 '20 at 17:49
  • @Bergi, sure, but it's just a stress test. In RL iterating like this over object with only 10 props is already 10 times slower that it could be. It's a bummer if you need, for example, just filter out empty objects out of several thousands. And yes `map.keys()` returns _iterator_, so no surprise here, but I wondered why `Object.keys()` return an array instead from the moment this function was introduced into the specs. – x00 Dec 05 '20 at 18:57
  • @x00 Even if the runtime complexity is `O(n)` (with `n` being the number of properties), that does *not* imply enumerating an object with 10 properties is exactly 10 times as slow as enumerating an object with 1 property. The exact runtime might be something like `345 + 678*log(n) + n/35`, which is in `O(n)` but could be assumed constant for all reasonably-sized objects, which might be the reason it is not further optimised by the engines. I'm looking for @jmrk to post an answer with more insights to what v8 does exactly and why. – Bergi Dec 05 '20 at 19:39
  • 1
    @x00 If you're wondering why `Object.keys` returns an array, that's because it was introduced in ES5 before iterators were a thing, and couldn't be changed later on. Instead, engines now optimise patterns like `Object.keys(…).length` and `for (const … of Object.keys(…))` when they spot them. – Bergi Dec 05 '20 at 19:40

1 Answers1

4

Not much to add here beyond what's already been discussed in comments...

See also my answer here: Object.keys() complexity?

for..in is a particularly complex operation as it even takes the prototype chain into account. for..of is potentially faster; but for (... of obj.keys()) is still at least linear, because Object.keys() returns an array.

Shortcutting the creation of temporary arrays is often impossible because the loop's body could modify the object, and the language spec usually states precisely what should happen when it does: usually such modifications must not be reflected in the iterations, so the engine indeed has to copy the list of properties first.

I'm not aware of a way to avoid this in a generic way (for all objects).

Beware of microbenchmarks, as they are often misleading, unless you already know what's going on under the hood and why. It's a safe bet that all engines treat objects with a million properties different from objects with ten properties, so what you investigate about the former shouldn't influence your decisions about the latter.

jmrk
  • 34,271
  • 7
  • 59
  • 74
  • Can you give some revealing links to the code, please? – x00 Dec 08 '20 at 15:24
  • 1
    Object.keys fast path: https://source.chromium.org/chromium/chromium/src/+/master:v8/src/builtins/builtins-object-gen.cc;l=457?q=ObjectKeys slow fallback: https://source.chromium.org/chromium/chromium/src/+/master:v8/src/runtime/runtime-object.cc;l=214?q=runtime_objectkeys Generally speaking, various features have various slow and fast paths all over the codebase; there's rarely a single piece of engine code responsible for a given JS feature, especially if it's a heavily used and heavily optimized feature. – jmrk Dec 09 '20 at 00:03