for-in vs Object.keys forEach without inherited properties
Asked Answered
R

7

54

I was looking at a perf benchmark of Object.keys + forEach vs for-in with normal objects.

This benchmark shows that Object.keys + forEach is 62% slower than the for-in approach. But what if you don't want to get the inherited properties? for-in includes all non-native inherited objects, so we'll have to use hasOwnProperty to check.

I tried to make another benchmark here doing exactly that. But now the for-in approach is 41% slower than Object.keys + forEach.


update

The above test was done in Chrome. Tested it again but with Safari and I'm getting different results: Object.keys(..).forEach(..) 34% slower, odd.

Note: The reason I'm benchmarking is to check how it is with Node.js.

Questions:

  • Are the jsperf result for Chrome considerable for Node.js?
  • What happened, how come a single conditional made the for-in approach 41% slower than Object.keys + forEach in Chrome?
Ronni answered 31/7, 2014 at 7:8 Comment(2)
Can you highlight the question(s) here? There seems to be quite a few to be answered.Gilburt
Updated my question. Sorry about the confusion.Ronni
F
44

node.js uses V8, although I guess it's not the same as the current version in Chrome, but I guess it's a good indicator of node's performances on the subject.

Secondarily, you're using forEach, which is quite handy when developing but adds a callback for every iteration, and that's a (relatively) lenghty task. So, if you're interested in performances, why don't you just use a normal for loop?

for (var i = 0, keys = Object.keys(object); i < keys.length; i++) {
    // ...
}

This yields the best performances you can get, solving your speed problems in Safari too.

In short: it's not the conditional, it's the call to hasOwnProperty that makes a difference. You're doing a function call at every iteration, so that's why for...in becomes slower.

Freedman answered 31/7, 2014 at 7:24 Comment(11)
Yes, but the second test I did. Shows that forEach is a lot faster. Considering for-in should not include inherited properties.Ronni
Added this snippet to the benchmark. It holds the top place against the 2. Thanks.Ronni
By the way, having Object.keys() called in condition section of loop is not good idea because it will be called in each iteration.Casemate
@Casemate I don't know if you're referring to my snippet above, but that's not what happened because of that precise reason.Freedman
Ah. yeah sorry. it is actually in initialization part. Then it is completely right. And you are right, using for loop instead of foreach will speed up execution, because it does not create a closure every iteration and you can break from it.Casemate
I guess replacing ; i < keys.length; to , il = keys.length; i < il; will improve performance even more.Hairsplitter
@Hairsplitter Performance tests actually show no noticeable improvements doing that. Even removing the increment part or other similar attempts are all negligeable. This is basically the fastest you can get, so better keep code readability and maintenability.Freedman
@Freedman so, how do I access a key at position i ?? your code example doesn't mentions it and I need to know howSowder
@Sowder Are you asking how to access to an element of an array in JavaScript? Simply with keys[i]. Your question, though, hints that you either need to read more carefully, or deepen your basic knowledge in JavaScript. (Or, possibly, explain better what you mean.)Freedman
@Freedman @Hairsplitter It used to significantly reduce loop performance to access the length property with every iteration, hence the , len=keys.length; i<len; trope.Carbine
@RoyTinker I know why Eadel use that - but, as I said back then, "performance tests actually show no noticeable improvements doing that". Actually, more than "no noticeable improvements", there could be no differences at all, as the compiler will probably yield the exact same thing. In short: don't try to apply performance tricks on your code, JavaScript isn't a low level language and you should let compilers do that job for you. Or else, things could even get worse as compilers evolve (yes, that happened too).Freedman
D
23

Just to note that:

var keys = Object.keys(obj), i = keys.length;

while(--i) {
   //
}

will not run for the index 0 and then you'll miss one of your attributes.

This in an array like ["a","b","c","d"] will run only d,c,b, and you'll miss the "a" 'cause index is 0 and 0 is false.

You need to decrement after the while check:

var keys = Object.keys(obj), i = keys.length;

while(i--) {
   //
}
Derward answered 31/1, 2016 at 18:26 Comment(0)
W
3

I was interested in this today as well, but mostly because I don't like having to test with hasOwnProperty to pass the default lint when I already know my objects are clean (as they've been created from object literals). Anyway, I expanded a little on the answer by @styonsk to include a better output and to run multiple tests and return the output.

Conclusion: It's complicated for node. The best time looks like using Object.keys() with either a numerical for loop or a while loop on nodejs v4.6.1. On v4.6.1, the forIn loop with hasOwnProperty is the slowest method. However, on node v6.9.1 it is the fastest, but it is still slower than both Object.keys() iterators on v4.6.1.

Notes: This was run on a late 2013 MacBook Pro with 16GB ram and a 2.4Ghz i5 processor. Every test pegged 100% of a single cpu for the duration of the test and had an average rss of about 500MB and peaked at 1GB of rss. Hope this helps someone.

Here are my results running against nodejs v6.9.1 and v4.6.1 with large objects (10^6 properties) and small objects(50 properties)

Node v4.6.1 with large object 10^6 properties

testObjKeyWhileDecrement Test Count: 100 Total Time: 57595 ms Average Time: 575.95 ms

testObjKeyForLoop Test Count: 100 Total Time: 54885 ms Average Time: 548.85 ms

testForInLoop Test Count: 100 Total Time: 86448 ms Average Time: 864.48 ms

Node v4.6.1 with small object 50 properties

testObjKeyWhileDecrement Test Count: 1000 Total Time: 4 ms Average Time: 0.004 ms

testObjKeyForLoop Test Count: 1000 Total Time: 4 ms Average Time: 0.004 ms

testForInLoop Test Count: 1000 Total Time: 14 ms Average Time: 0.014 ms

Node v6.9.1 with large object 10^6 properties

testObjKeyWhileDecrement Test Count: 100 Total Time: 94252 ms Average Time: 942.52 ms

testObjKeyForLoop Test Count: 100 Total Time: 92342 ms Average Time: 923.42 ms

testForInLoop Test Count: 100 Total Time: 72981 ms Average Time: 729.81 ms

Node v4.6.1 with small object 50 properties

testObjKeyWhileDecrement Test Count: 1000 Total Time: 8 ms Average Time: 0.008 ms

testObjKeyForLoop Test Count: 1000 Total Time: 10 ms Average Time: 0.01 ms

testForInLoop Test Count: 1000 Total Time: 13 ms Average Time: 0.013 ms

And following is the code I ran:

//Helper functions
function work(value) {
  //do some work on this value
}

function createTestObj(count) {
  var obj = {}
  while (count--) {
    obj["key" + count] = "test";
  }
  return obj;
}

function runOnce(func, obj) {
  var start = Date.now();
  func(obj);
  return Date.now() - start;
}

function testTimer(name, func, obj, count) {
  count = count || 100;
  var times = [];
  var i = count;
  var total;
  var avg;

  while (i--) {
    times.push(runOnce(func, obj));
  }

  total = times.reduce(function (a, b) { return a + b });
  avg = total / count;

  console.log(name);
  console.log('Test Count: ' + count);
  console.log('Total Time: ' + total);
  console.log('Average Time: ' + avg);
  console.log('');
}

//Tests
function testObjKeyWhileDecrement(obj) {
  var keys = Object.keys(obj);
  var i = keys.length;
  while (i--) {
    work(obj[keys[i]]);
  }
}

function testObjKeyForLoop(obj) {
  var keys = Object.keys(obj);
  var len = keys.length;
  var i;
  for (i = 0; i < len; i++) {
    work(obj[keys[i]]);
  }
}

function testForInLoop(obj) {
  for (key in obj) {
    if (obj.hasOwnProperty(key)) {
      work(obj[key]);
    }
  }
}

//Run the Tests
var data = createTestObj(50)
testTimer('testObjKeyWhileDecrement', testObjKeyWhileDecrement, data, 1000);
testTimer('testObjKeyForLoop', testObjKeyForLoop, data, 1000);
testTimer('testForInLoop', testForInLoop, data, 1000);
Wingspan answered 25/11, 2016 at 5:5 Comment(2)
I just tested Object.keys(obj).forEach(work) and it fits squarely between forIn and Object.keys with numerical for and while loops.Wingspan
I put together a jsperf for this here: jsperf.com/object-property-iterationWingspan
F
1

I added more for loops to @Hominiminim's code, and here are my results ran in Angular on Google Chrome (lower is better):

  • For: 1209
  • For In: 1184
  • For Of (Object.entries): 2115
  • For Of (Object.keys): 1220
  • For Of (Object.values): 1571
  • For Each (Object.entries): 1776
  • For Each (Object.keys): 1228
  • For Each (Object.values): 1475

If anyone wants to try for themselves, here's the code in jsfiddle: https://jsfiddle.net/hikarii_flow/295v7sb3/

Additionally, here are my results from jsfiddle:

  • For: 477
  • For In: 458
  • For Of (Object.entries): 1361
  • For Of (Object.keys): 493
  • For Of (Object.values): 817
  • For Each (Object.entries): 1123
  • For Each (Object.keys): 489
  • For Each (Object.values): 806

function work(value) {
    const arr = Array.from(value);
    const arr2 = arr.reverse();
    const arr3 = arr2.reverse();
    const test = arr3.indexOf('y');
}

function createTestObj(count) {
    let obj = {};

    while (count--) {
        obj["key" + count] = "test";
    }

    return obj;
}

function For(obj) {
    const start = Date.now()

    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
        work(obj[keys[i]]);
    }

    console.log("For: " + (Date.now() - start));
}

function ForIn(obj) {
    const start = Date.now()

    for (const key in obj) {
        work(obj[key]);
    }

    console.log("For In: " + (Date.now() - start));
}

function ForOfEntries(obj) {
    const start = Date.now();

    for (const [key, value] of Object.entries(obj)) {
        work(value);
    }

    console.log("For Of (entries): " + (Date.now() - start));
}

function ForOfKeys(obj) {
    const start = Date.now();

    for (const key of Object.keys(obj)) {
        work(obj[key]);
    }

    console.log("For Of (keys): " + (Date.now() - start));
}

function ForOfValues(obj) {
    const start = Date.now();

    for (const value of Object.values(obj)) {
        work(value);
    }

    console.log("For Of (values): " + (Date.now() - start));
}

function ForEachEntries(obj) {
    const start = Date.now();

    Object.entries(obj).forEach(kvp => {
        const [key, value] = kvp;
        work(value);
    });

    console.log("For Each (entries): " + (Date.now() - start));
}

function ForEachKeys(obj) {
    const start = Date.now();

    Object.keys(obj).forEach(key => {
        work(obj[key]);
    });

    console.log("For Each (keys): " + (Date.now() - start));
}

function ForEachValues(obj) {
    const start = Date.now();

    Object.values(obj).forEach(value => {
        work(value);
    });

    console.log("For Each (values): " + (Date.now() - start));
}

//Run the Tests
const data = createTestObj(1000000);
For(data);
ForIn(data);
ForOfEntries(data);
ForOfKeys(data);
ForOfValues(data);
ForEachEntries(data);
ForEachKeys(data);
ForEachValues(data);
Faggot answered 24/6, 2023 at 7:9 Comment(0)
V
0

For anyone viewing still concerned with iterating object properties in JS, the absolute fastest method is:

var keys = Object.keys(obj), i = keys.length;

while(--i) {
   //
}

http://jsperf.com/object-keys-foreach-vs-for-in-hasownproperty/8

You can save a bit on large objects by not having to recompute the length value of the key array (mostly negligible with modern browser optimizations), which is also true for the case of a simple for loop. The decremented while loop is still faster than the for loop or the incremented while loop with length upper limit comparison, by a fair margin.

Vagabondage answered 22/9, 2015 at 1:38 Comment(1)
Pre decrement check breaks the condition at index 0. See answer https://mcmap.net/q/336437/-for-in-vs-object-keys-foreach-without-inherited-propertiesBlameless
U
0

I tested this today. For my purposes, getting the Object keys and then doing a plain old for loop was faster than doing a decrementing while or a for in loop. Feel free to change this template to test the different loops out for your individual case:

//Helper functions
function work(value) {
  //do some work on this value
}

function createTestObj(count) {
  var obj = {}
  while (count--) {
    obj["key" + count] = "test";
  }
  return obj;
}

//Tests
function test_ObjKeyWhileDecrement(obj) {
  console.log("Time Started: ", new Date().getTime());
  var keys = Object.keys(obj),
    i = keys.length;
  while (i--) {
    work(obj[keys[i]]);
  }
  console.log("Time Finished: ", new Date().getTime());
}

function test_ObjKeyForLoop(obj) {
  console.log("Time Started: ", new Date().getTime());
  for (var i = 0, keys = Object.keys(obj); i < keys.length; i++) {
    work(obj[keys[i]]);
  }
  console.log("Time Finished: ", new Date().getTime());
}

function test_ForInLoop(obj) {
  console.log("Time Started: ", new Date().getTime());
  for (key in obj) {
    work(obj[key]);
  }
  console.log("Time Finished: ", new Date().getTime());
}

//Run the Tests
var data = createTestObj(1000 * 100)
console.log("Test Obj Key While Decrement Loop")
test_ObjKeyWhileDecrement(data);
console.log("Test Obj Key For Loop")
test_ObjKeyForLoop(data);
console.log("Test For In Loop")
test_ForInLoop(data);

You may want to run that in your actual environment to test instead of in a jsfiddle. Try multiple browsers also.

Uric answered 7/3, 2016 at 15:55 Comment(1)
You should use console.time and console.timeEnd for such cases.Paleethnology
P
-1

And for the ES6 fans out there, looks like

Object.keys(obj).reduce((a,k) => {a += obj[k]; return a}, res)

is by far the fastest.

https://jsperf.com/for-in-vs-for-of-keys-vs-keys-reduce

Patrology answered 13/10, 2017 at 2:52 Comment(1)
2018: keys-reduce is only 0.5% faster on V8, but 2% slower on FireFox.Venicevenin

© 2022 - 2024 — McMap. All rights reserved.