Thread Tools Display Modes
07-06-12, 05:40 PM   #1
Haleth
This Space For Rent
 
Haleth's Avatar
Featured
Join Date: Sep 2008
Posts: 1,173
A question on memory allocation/access

I'm wondering a couple of things about lua, some of them specific to WoW (I think), that have been bothering me for a while.

1. Take this example code:

Code:
local function myFunction()
	local name = GetSomeName()
	-- do other things
end
Assume the function is used multiple times. Is this any less efficient than:

Code:
local name
local function myFunction()
	name = GetSomeName()
	-- do other things
end
I assume that in the first case, a new variable is allocated each time the function runs. But is it cleared up as soon as the function finishes, perhaps by an automatic garbage collector? Otherwise, the second example (using the same variable and simply changing its content) would be more efficient, right?

2. A similar example (assume this function is called multiple times as well):

Code:
local function myFunction()
	myFrame:SetScript("OnUpdate", function()
		-- do things
	end)
	-- do other things
	myFrame:SetScript("OnUpdate", nil)
end
Is this less efficient than:

Code:
local function onUpdate()
	-- do things
end

local function myFunction()
	myFrame:SetScript("OnUpdate", onUpdate)
	-- do other things
	myFrame:SetScript("OnUpdate", nil)
end
3. In for-loops, if your end condition is the return value of a function, does the loop run this function on every cycle to compare the current index against it or is the return value stored at the start of the loop? In other words, is this:

Code:
local myNumber = GetSomeNumberFromServer()
for i = 1, myNumber do
	-- do things
end
More efficient than this:

Code:
for i = 1, GetSomeNumberFromServer() do
	-- do things
end
4. I know that importing global variables (such as _G) into your code can speed things up a tiny tiny bit when you use them often. But how is this? If I'm correct, tables are passed by reference. How would creating an other reference which is the same as the existing one make things any faster?

Thanks!

Last edited by Haleth : 07-06-12 at 05:47 PM.
  Reply With Quote
07-06-12, 05:56 PM   #2
Phanx
Cat.
 
Phanx's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2006
Posts: 5,617
You're lucky my browser restores the contents of form fields when I hit Back. Otherwise I'd be pretty pissed to have spent 20 minutes answering your questions only to be told that the thread did not exist after I hit Submit.

----------------------------------------------------------------------------------------------------------------------

(1) Declaring a variable inside a function vs. outside

(a)
Code:
local function myFunction()
	local name = GetSomeName()
	-- do other things
end
(b)
Code:
local name
local function myFunction()
	name = GetSomeName()
	-- do other things
end
Short answer:
I'd recommend (a), not (b), but there is no significant difference, so do what you want.

Long answer:
In terms of total memory/CPU usage, these are functionally identical. Both will create a new value (returned by the GetSomeName call) and create a pointer to it named "name" each time the function is run. What is different is when that value will be garbage-collected. In version (a) the value is discarded as soon as the function's execution finishes. In version (b) the value is held in memory until the next time the function is executed, and then discarded when the "name" pointer is redirected to the new value. From a code-readability perspective, I personally think (a) is cleaner and easier to read, too.

----------------------------------------------------------------------------------------------------------------------

(2) Defining a function inside another function

(a)
Code:
local function myFunction()
	myFrame:SetScript("OnUpdate", function()
		-- do things
	end)
	-- do other things
end
(b)
Code:
local function onUpdate()
	-- do things
end

local function myFunction()
	myFrame:SetScript("OnUpdate", onUpdate)
	-- do other things
end
Short answer:
Version (a) is bad. Don't do that. Functions are expensive to create. Do (b) instead.

Long answer:
There are a couple of specific situations where (a) is okay.

(i) You are only calling "myFunction" once the whole time your addon is loading and running. If you're calling it more than once, define the OnUpdate script handler function outside of "myFunction". If you're not sure whether you'll call it more than once, do (b) instead.

(ii) You are using "myFunction" to construct a custom function based on other variables that are also defined inside "myFunction". Many addons using AceConfig options tables do this. Here's an example from Grid that creates some dynamic options:

Code:
	for class, name in pairs(classes) do
		local class, name = class, name
		auraOptions.class.args[class] = {
			name = name,
			desc = string.format(L["Show on %s players."], name),
			type = "toggle",
			get = function()
					return GridStatusAuras.db.profile[status][class] ~= false
				end,
			set = function(_, v)
					GridStatusAuras.db.profile[status][class] = v
					GridStatusAuras:UpdateAllUnitAuras()
				end,
		}
	end
If you're not doing either of those things, you should use (a).

----------------------------------------------------------------------------------------------------------------------

(3) Function return values in for loop parameters

(a)
Code:
local myNumber = GetSomeNumberFromServer()
for i = 1, myNumber do
	-- do things
end
(b)
Code:
for i = 1, GetSomeNumberFromServer() do
	-- do things
end
No functional difference. "GetSomeNumberFromServer" will only be called once. This is clearly stated in the Lua manual section on for loops.

In general, I'd recommend (b) since it is less code to read. The only reason to do (a) would be if you need to use the same value more than once:

Code:
local n = GetNumRaidMembers()
if n > 0 then
	for i = 1, n do
		-- do things
	end
end
__________________
Retired author of too many addons.
Message me if you're interested in taking over one of my addons.
Don’t message me about addon bugs or programming questions.
  Reply With Quote
07-06-12, 07:09 PM   #3
Haleth
This Space For Rent
 
Haleth's Avatar
Featured
Join Date: Sep 2008
Posts: 1,173
Thanks for those replies, a very interesting read.

I figured it'd be cleaner if I made an own topic rather than leaving a PM quote. I don't know of any browser these days that doesn't cache posts (maybe IE), but sorry.

What do you think about (4)?
  Reply With Quote
07-06-12, 07:56 PM   #4
Phanx
Cat.
 
Phanx's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2006
Posts: 5,617
I sometimes have to use Firefox or Chrome at work, and have lost entered form data on numerous occasions with both. It may depend partly on the type of form, I suppose.

As for #4, my lunch hour was over, and the boss was lurking.

(4) Local copies of global variables

_G is actually a special case in that it's a global variable that points to a table containing all global variables. Accessing it is a global lookup. In order from slowest to fastest:
  1. print(_G.x)
    • global lookup + table lookup every time
  2. print(x)
    • global lookup every time
  3. local _G = _G; print(_G.x)
    • global lookup once
    • local lookup + table lookup every time
  4. local x = _G.x; print(x)
    • global lookup + table lookup once
    • local lookup every time
  5. local x = x; print(x)
    • global lookup once
    • local lookup every time
When you assign a variable as the value of a new variable, what happens depends on what kind of value the original variable had. If it's a string, a number, or a boolean, then it's passed by value: the two variables are not connected, and point to unique objects (basically). If it's a table or a function, then it's passed by reference: both variables point to the same object.

As for the original question -- how a local copy of a global variable is faster -- consider this global table "x":

Code:
x = {
	cat = "sensory overload",
	dog = "fate accepted",
	lizard = "lizard",
}
You can look up values in it:

Code:
print(x["cat"])
This costs 1 global lookup (to find "x") and 1 table lookup (to find the "cat" key in "x").

Code:
local x = x
print(x["cat"])
This costs 1 local lookup (to find "x") and 1 table lookup.

You can't avoid the table lookup (unless you're only ever interested in one value in the table, in which case you should just do "local var = x["cat"]" at the top of your file and use "var" instead of looking up "x["cat"]" every time. What you can do is replace the slow global lookup with a fast local lookup. That's why you should always create local upvalues to global tables/functions if you're going to access them very often.

If you're only calling the function or looking up a value in the table when the player opens an options window, or in response to events like PLAYER_ALIVE or OnClick, you don't really need to bother, because the speed increase is pointless.

However, if you're calling or looking up inside an OnUpdate script, or in a function that's called very frequently such as an event handler for COMBAT_LOG_EVENT_UNFILTERED or UNIT_HEALTH, then all of those infinitesimal speed increases add up, especially in real-life situations where users are running 5, 10, 50, or 100 addons at the same time.
__________________
Retired author of too many addons.
Message me if you're interested in taking over one of my addons.
Don’t message me about addon bugs or programming questions.

Last edited by Phanx : 07-06-12 at 07:59 PM.
  Reply With Quote
07-06-12, 08:22 PM   #5
Haleth
This Space For Rent
 
Haleth's Avatar
Featured
Join Date: Sep 2008
Posts: 1,173
I understand that global 'lookups' are slower than local ones - I'm interested as to why exactly though, considering they point to the same memory location in the case of tables. If I'm guessing correctly, are variables stored in hash tables and is the table for global variables slower to search than the one used for locals?

Thanks for the insightful post btw - will be saving this for future reference.
  Reply With Quote
07-06-12, 10:37 PM   #6
Torhal
A Pyroguard Emberseer
 
Torhal's Avatar
AddOn Author - Click to view addons
Join Date: Aug 2008
Posts: 1,196
The reference to the memory location is located SOMEWHERE within the global table (which also has to be looked up and found!) - you have to find that reference. Sure, it always points to the same memory location but unless you store that location in a variable you're continually looking it up (since Lua has no mechanism for saying "Oh, we already found this - I'll remember where it was!" unless you tell it to do so by using a variable.)
__________________
Whenever someone says "pls" because it's shorter than "please", I say "no" because it's shorter than "yes".

Author of NPCScan and many other AddOns.
  Reply With Quote
07-07-12, 02:39 AM   #7
Phanx
Cat.
 
Phanx's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2006
Posts: 5,617
Think of the global table as a book, and you want to find your favorite passage to read it to your friend. You can either look over the shelves to find the book, and then flip through the book to find the passage -- or you can leave the book open on your coffee table open to the relevant page with the passsage highlighted.
__________________
Retired author of too many addons.
Message me if you're interested in taking over one of my addons.
Don’t message me about addon bugs or programming questions.
  Reply With Quote
07-07-12, 07:03 AM   #8
Ketho
A Pyroguard Emberseer
 
Ketho's Avatar
AddOn Author - Click to view addons
Join Date: Mar 2010
Posts: 1,026
Thanks Phanx for the very useful pointers

(This also made me remember the old WowAce Codings Tips page)
  Reply With Quote
07-11-12, 04:28 PM   #9
Haleth
This Space For Rent
 
Haleth's Avatar
Featured
Join Date: Sep 2008
Posts: 1,173
Right - thanks for the replies.

One other thing I've come across - freeing up the memory allocated to a table. I've tried looking this up but I can't find anything on the actual memory usage.

I need a table that can hold temporary tables for an addon I'm making. When I'm done with these tables, I want all of the memory used to be freed up again.

Code:
local myTable = {}
function testTable()
	for i = 1, 2000 do
		myTable[i] = {"string", "otherstring", 42, true}
	end
end

function testClearTable()
	for i = 1, 2000 do
		myTable[i] = nil
	end
end
When I run the testTable() function, the memory shoots up from 1kb to about 450kb. When I run testClearTable(), the memory falls back down to about 50kb - so not all memory is freed up.

When I do this, however:

Code:
local myTable = {}
function testTable()
	for i = 1, 2000 do
		myTable[i] = {"string", "otherstring", 42, true}
	end
end

function testClearTable()
	myTable = nil
end
The memory does get freed up properly.

Am I correct in assuming that the only way to correctly 'empty' a table is to re-assign the variable to a new table?

Last edited by Haleth : 07-11-12 at 06:26 PM. Reason: Typo
  Reply With Quote
07-11-12, 06:11 PM   #10
Brusalk
An Aku'mai Servant
 
Brusalk's Avatar
AddOn Author - Click to view addons
Join Date: May 2010
Posts: 32
I could very well be wrong, and I expect to be corrected if I am, but in the first example you're setting the elements in the table to nil, but not doing anything to the actual table.

In the second table you're just getting rid of the table entirely, so my guess is that the extra 49kb you're seeing is the table itself (which at this point is just {} after setting all of those elements to nil)
  Reply With Quote
07-11-12, 07:45 PM   #11
SDPhantom
A Pyroguard Emberseer
 
SDPhantom's Avatar
AddOn Author - Click to view addons
Join Date: Jul 2006
Posts: 2,323
The way Lua stores tables, it reserves 2 arrays per table, one holds sequential indices and the other holds associative ones. When needed, Lua expands these arrays in powers of 2, but they never shrink back down when a value in the table is set to nil. This means when you build a table, then iterate through it to set everything to nil, the allocated space for the indices are still there. Usually, when you release a table, it stays in memory until the garbage collector comes to deallocate the memory used. You can force the garbage collector by calling collectgarbage("collect"). This will halt execution for the garbage collector to run, so it's not wise to use often or in moments of high CPU usage. Alternatively, Blizzard added the wipe() function to the API. I don't know how far it goes as far as cleaning up used memory. That might be something to test.
__________________
WoWInterface AddOns
"All I want is a pretty girl, a decent meal, and the right to shoot lightning at fools."
-Anders (Dragon Age: Origins - Awakening)

Last edited by SDPhantom : 07-11-12 at 07:48 PM.
  Reply With Quote
08-17-12, 09:53 PM   #12
nailertn
An Aku'mai Servant
Join Date: Oct 2008
Posts: 33
Originally Posted by SDPhantom View Post
The way Lua stores tables, it reserves 2 arrays per table, one holds sequential indices and the other holds associative ones. When needed, Lua expands these arrays in powers of 2, but they never shrink back down when a value in the table is set to nil. This means when you build a table, then iterate through it to set everything to nil, the allocated space for the indices are still there.
You can shrink tables by forcing a rehash:
Code:
t = {}
for i = 1,100 do
	t[i] = true
end

-- t is 2084 bytes

for i = 1,100 do
	t[i] = nil
end

-- t is 2084 bytes

t.a = nil

-- t is 76 bytes
Table sizes are not necessarily powers of two:

Code:
t = { true, true, true }

-- t is 84 bytes (36 + 3 * 16)

t = {}
t[1] = true
t[2] = true
t[3] = true

-- t is 100 bytes (36 + 4 * 16)
However the same does not work with hash tables:

Code:
t = { a = true, b = true, c = true }

-- t is 196 bytes (36 + 4 * 40)

t = {}
t.a = true
t.b = true
t.c = true

-- t is 196 bytes (36 + 4 * 40)
Explanation (skip to the part about tables)
  Reply With Quote
08-18-12, 03:00 PM   #13
SDPhantom
A Pyroguard Emberseer
 
SDPhantom's Avatar
AddOn Author - Click to view addons
Join Date: Jul 2006
Posts: 2,323
What version of Lua are you talking about? As of now, WoW runs 5.1, the allocation for tables may be different in newer versions. Also, my explanation the subject wasn't memory size, but the number of elements allocated in the 2 arrays of a table.



http://www.wowpedia.org/Memory_usage.
Memory accumulation isn't linear however, instead there is an exponential algorithm, so that as the table gets larger, incrementally larger numbers of elements are pre-allocated.


http://www.wowpedia.org/Lua_object_memory_sizes
A new, empty table will consume 40 bytes.

As indexes are added, the table will allocate space for new indexes at an exponential rate. For example:

Code:
local t = {}  -- 40 bytes
t[1] = true   -- alloc 1 index
t[2] = true   -- alloc 1 index
t[3] = true   -- alloc 2 indexes
t[4] = true
t[5] = true   -- alloc 4 indexes
...
If a table is indexed by sequential integers, each index will take 16 bytes (not including any memory allocated for the value). If the becomes a hash, the index size will jump to 40 bytes each. Lua is "smart" and will allocate at the 16 byte rate as much as it can. If the int sequence is broken, the new values will allocate at the 40 byte rate. For example:

Code:
local t = {}  -- 40 bytes
t[1] = true   -- 16 bytes
t[2] = true   -- 16 bytes
t[3] = true   -- 32 bytes
t[4] = true
t[8] = true   -- 40 bytes
t[9] = true   -- 40 bytes
t[10] = true  -- 80 bytes
t["x"] = true
t["y"] = true -- 160 bytes
...
__________________
WoWInterface AddOns
"All I want is a pretty girl, a decent meal, and the right to shoot lightning at fools."
-Anders (Dragon Age: Origins - Awakening)

Last edited by SDPhantom : 08-18-12 at 03:10 PM.
  Reply With Quote
08-18-12, 04:13 PM   #14
nailertn
An Aku'mai Servant
Join Date: Oct 2008
Posts: 33
Originally Posted by SDPhantom View Post
What version of Lua are you talking about? As of now, WoW runs 5.1, the allocation for tables may be different in newer versions. Also, my explanation the subject wasn't memory size, but the number of elements allocated in the 2 arrays of a table.
All of that is directly from WoW using collectgarbage("count"). Both of us are talking about table size. Memory allocation is a convenient way to measure it.
  Reply With Quote

WoWInterface » Developer Discussions » Lua/XML Help » A question on memory allocation/access

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off