Log In  

Let's say I have a table called 'objects' that contains all of the objects in my scene. I want to loop through it and call each object's update function. The order in which the objects are updated doesn't matter. Of the following examples:

-- example 1
for obj in all(objects) do
  obj:update()
end

-- example 2
for _,obj in pairs(objects)
  obj:update()
end

-- example 3
for i in pairs(objects)
  objects[i]:update()
end

-- example 4
foreach(objects, function(obj)
  obj:update()
end)

which is the fastest?

Thanks.

P#46558 2017-11-21 16:37 ( Edited 2017-11-26 18:32)

I believe pairs() has been found to be fastest, by test.

P#46559 2017-11-21 16:49 ( Edited 2017-11-21 21:49)

Interesting. Do you know if there's any difference between my examples 2 and 3?

P#46560 2017-11-21 17:42 ( Edited 2017-11-21 22:42)

No, sorry, haven't tested that. @Felice might know, though.

P#46561 2017-11-21 18:01 ( Edited 2017-11-21 23:01)
1

Well, I just did my own experiment. Here's my code:

objects = {}

for i=1,20000 do
  add(objects, {
    age = 0,
    update = function(self)
      self.age += 1
    end})
end

start = time()

-- for obj in all(objects) do
--   obj:update()
-- end

-- for _,obj in pairs(objects) do
--   obj:update()
-- end

-- for i in pairs(objects) do
--   objects[i]:update()
-- end

-- foreach(objects, function(obj)
--   obj:update()
-- end)

print(time()-start)

I just uncommented each method and ran the cart one at a time. Here are the results:

for obj in all(objects): 0.1 seconds
for _,obj in pairs(objects): 0.0333 seconds
for i in pairs(objects): 0.0667 seconds
foreach(objects, ...): 0.1333 seconds

pairs() was indeed the fastest by a lot, and surprisingly foreach() was the slowest. Was my methodology flawed somehow? I've definitely heard people saying foreach() is faster than all(). Maybe the use of an anonymous function has something to do with it?

P#46562 2017-11-21 19:48 ( Edited 2017-11-22 00:48)
1

I'm late to the punch here, but your numbers pretty much agree with what I would have said anyway.

Yes, the anonymous call overhead is the added cost in your specific usage of foreach(). There are a handful of cycles of overhead in any function call, so the added anonymous wedge function introduces quite a bit vs. the single cycle of overhead per pairs() or numeric loop iteration.

If you weren't using object-oriented code, it would be faster. If you had just a global update() vs the per-object method, e.g.:

foreach(objects, update)

Then it would probably be about the same as the pairs() implementation, since I think that's what foreach() uses internally.

Of course, you almost certainly want that to be a per-object method, rather than a global call, so foreach() isn't your friend in this case.

For calling methods, maybe there should be (or you could write) a variation called oneach(), something like this:

function oneach(table,method_name)
  for _,obj in pairs(table) do
    obj[method_name](obj)
  end
end
...
oneach(objects,"update")

If I remember my research correctly, these two have the same cost (and effect):

obj:func()
obj["func"](obj)

If so, that means this hypothetical oneach() should be as cheap as an inline pairs() loop, with lower token cost, at least for direct method calls.

I do encourage you to double-check that, though, as my research was on the previous version of the app and zep said he rebalanced some of the cycle costs.

P#46574 2017-11-22 10:34 ( Edited 2017-11-22 15:43)

Okay, I actually tested my implementation in your framework. I, uh, had to fix it. The code I gave above is now corrected. ;)

It comes out to 0.0333, same as the inline pairs() version.

So yeah, that might be handy. I think I'll use it myself. Too bad I probably can't convince zep to add it. :)

P#46575 2017-11-22 10:39 ( Edited 2017-11-22 15:46)

Thanks for your research. I like that oneach() method a lot. It's performant, saves tokens and is more readable than pairs(), which is perfect. I think I'll use it if you don't mind.

P#46582 2017-11-22 12:12 ( Edited 2017-11-22 17:15)

Feel free, of course. :) It's a shame the method name has to be a string, but otherwise I'm liking it already in my own code.

By the way, I picked "oneach" out of the air, really. Anyone have a better name?

(I mean, "oneach" will do fine, but maybe there's something even clearer, or there's some other language that has a similar construct already and we can steal their terminology.)

P#46597 2017-11-22 19:52 ( Edited 2017-11-23 00:52)

Could go with forall() to make it slightly more natural to read, kind of like the for...in all() construct.

forall(enemies, "draw")
P#46626 2017-11-23 11:11 ( Edited 2017-11-23 16:11)

Yeah, I considered that, but the all() iterator implies that objects are traversed in order, and this function uses pairs(), which doesn't do that. As terminology goes, it would be misleading.

(That's why using all() is expensive, btw. It has to order the unordered hash table.)

P#46651 2017-11-24 03:23 ( Edited 2017-11-24 08:23)

I've heard pairs() is unordered but from my experience that's only true when it's traversing named key,value pairs. If the table is indexed like one containing other tables, such as a table containing instanced game objects, it is ordered.

For instance:

t = {{"a"},{"b"},{"c"}}
for k,v in pairs(t) do
  print(k..": "..v[1])
end

will always print:

> 1: a
> 2: b
> 3: c

edit: Also, if you are traversing a table with mixed indexed values and named key,value pairs pairs() will do the indexed values first.

P#46655 2017-11-24 04:02 ( Edited 2017-11-24 09:37)

That's true, from what I've seen. I'm guessing Lua uses the index value directly if it's a numeric index, and then if it has to calculate a hash value, it does something to ensure they follow the indices, maybe setting the top bit or something.

Unfortunately, you can't assume a table is indexed, or not, or both.

One thing I'm not sure about is what a [0] entry looks like. You can fake zero-based arrays by setting arr[0] to something, or initializing like this:

arr = {[0]="zero","one","two","three", ...etc... }

But I'm not sure if the [0] initializer, which can actually come at the end and it'll work just the same, will be stored as a hashed entry or an indexed entry.

P#46672 2017-11-24 21:19 ( Edited 2017-11-25 02:21)
t = {[0]="x","a","b","c"}
for k,v in pairs(t) do
  print(k..": "..v)
end

1: A
2: B
3: C
0: X
t = {[1]="a",[2]="b",[3]="c"}
for k,v in pairs(t) do
  print(k..": "..v)
end

3: C
1: A
2: B
t = {}
t[2]="b"
t[3]="c"
t[0]="x"
t[1]="a"

for k,v in pairs(t) do
  print(k..": "..v)
end

3: C
1: A
2: B
0: X

I wouldn't trust pairs() too much...

P#46718 2017-11-26 12:11 ( Edited 2017-11-26 17:11)

Yeah, personally I always just assume pairs(t) effectively iterates using rnd(). ;) It's safest that way.

P#46721 2017-11-26 13:32 ( Edited 2017-11-26 18:32)

[Please log in to post a comment]