Wednesday, October 14, 2009

Local Globals - Are they worth it?

I've been a little.. undecided.. lately. In my pursuit to make Tweaker as efficient as possible, I've taken the approach to create local instances of globals -- for faster lookups. Lua checks the local namespace first, local to a function or block or whatever. If a variable isn't found, it starts searching up the hierarchy, eventually to the local file and, I believe, then to the global namespace. True lua might have more intermediary steps, but I think that's how things work for WoW.

By defining locally, all the global functions and tables and whatnot that I'll be using, I save Lua from having to take a step (or more) up. But when you're in an IF statement, that's inside a loop, that's inside a function -- it still needs to take those steps. How much am I really saving?

In the file's namespace, where I do my testing, it looks somewhat significant: roughly half the time. But we're still talking hundredths of a millisecond over the course of a thousand iterations.

Moreover, I have some uncertainty in how to do things. Currently, libraries (like string and table) have the library and method separated by an underscore: local string_trim = string.trim. Native lua functions, like loadstring and pairs, have their local names unchanged. local pairs = pairs. The global table is just renamed.. local g = _G. If the code calls string_join, an error is thrown if I've not set the local reference. It can be annoying getting a dozen errors, one at a time, because of this, but it's easy enough to make sure the locals are setup. loadstring and pairs are a bit harder -- I just need to sift through the code and make sure the local is defined for all these things, if I'm using them. The upside here is that intellisense works, the keywords are colored properly, because they're unchanged.

So both forms have their ups and downs. Perhaps that's why I use each. But the maintenance is getting to be a hassle. Moreover, I've realized my tests to be faulty. True enough, in the local file namespace, using the local is twice as fast. But how realistic is that? Most of your calls, especially the ones that need to be benchmarked because they're done over and over and over, are within functions, inside loops, and possibly branched with if statements. A more appropriate test shows that defining the locals in the local file provides roughly a 20% improvement in cpu usage. That's a nice number still, but a far cry from 200%, and I still wonder how nice it is when dealing with hundredths of a millisecond over thousands of runs. Is it worth it?

Python takes the approach that libraries are included in the block that uses them, whether that's the file, a function, a loop, an if statement, or any other block segment. I assume it's efficient for Python, but applying this concept to Lua locals is more hit or miss. The path still needs to be traversed in order to find the value, but now it's doing it on every function call, loop iteration, if statement, etc. and there's the overhead of the new local to store it. A single call is slightly less efficient, more calls add efficiency by comparison, but it's not likely you'll call the same function enough times to really make a gain here.

In my opinion, take it on a case by case basis. If you're calling the same function repeatedly in a block, you may want to make a local to it rather than having the global lookup again and again. If you're calling many functions from a single library in a block, make a local reference to the library: local string = string. But I'd wager that 99% of the time, it's not worth it. Stick with the normal calls. Enjoy your intellisense and your sanity!

Update: As for what's better, ("hello"):len() or string.len("hello"), I can't get any clear, consistent results. string.len tends to be slightly better more often, but the numbers jump so much I can't be definitive. However, the class.func format always works, while the obj:func format seems to only work for certain functions (myTable:getn() is nil, for example) so I'd stick with the class.func format.