WoW:HOWTO: Use Tables Without Generating Extra Garbage

From AddOn Studio
Jump to navigation Jump to search

WoW Lua

Creating number and strings values is pretty efficient in Lua, Numbers do not have any additional data structure associated with them and are freed immediately when no longer used. Strings are hashed and stacked together with same strings, so creating 10000 copies of "Hello, world!" or creating and freeing it repeatedly will not produce garbage. Tables, however, always take memory when created that always becomes garbage when table is no longer used. If you do not want to create unnecessary garbage either avoid using tables unless it is really necessary or, in case several of your throw-away tables use same set of fields, or each new calculation uses superset of field of previous table, use only one table instead of creating new table for every calculation. For example, if you need to sort 20 values, display result and then sort another 30 and display them too, most people would write:

local temporaryTable

temporaryTable={}
for idx=1, 20 do
 temporaryTable[idx]=GetSomeDataByIndex(idx)
end
DisplayResult(table.sort(temporaryTable))

temporaryTable={} -- inefficient
for idx=1, 30 do
 temporaryTable[idx]=GetSomeOtherDataByIndex(idx)
end
DisplayResults(table.sort(temporaryTable))

This example would create one table, trash it after calculation and create another throw-away table again. Removing extra table constructor in line marked with "-- inefficient" will save you memory. Such optimization would be especially effective inside long loops or frequently called function.

Old:

local function OftenCalledSortingFunction()
 local temporaryTable={}
 for idx=1, 20 do
  temporaryTable[idx]=GetSomeDataByIndex(idx)
 end
 return table.sort(temporaryTable)
end

Placing call to this in frequently called event or even worse in "OnUpdate" handler is a sure way to fill your memory with garbage fast.

New:

local workingTableForOftenCalledSortingFunction={} -- we can create it once and reuse it because we always sort exactly 20 values
local function OftenCalledSortingFunction()
 for idx=1, 20 do
  workingTableForOftenCalledSortingFunction[idx]=GetSomeDataByIndex(idx)
 end
 return table.sort(workingTableForOftenCalledSortingFunction)
end

You can futher maximize savings if some other functions too use table with same size/set of field by sharing this working table between them.

Here's example straight from many addons that create UI dropdowns:

local function MyAddon_InitMenu()
 local info
 
 info = {}
 info.text = "Settings Button1"
 info.value = "Some value"
 info.checked = PlayerSettings["Setting1"]
 UIDropDownMenu_AddButton(info);
 
 info = {};
 info.text = "Settings Button2"
 info.value = "Some other value"
 info.checked = PlayerSettings["Setting2"]
 UIDropDownMenu_AddButton(info);
 
 -- end of settings buttons
 
 info = {};
 info.text = "Action Button1"
 info.value = "Value to act on"
 info.func = function() DoSomething() end;
 UIDropDownMenu_AddButton(info);
 
 info = {};
 info.text = "Action Button2"
 info.value = "Another value to act on"
 info.func = function() DoSomethingElse() end;
 UIDropDownMenu_AddButton(info);
end

If you ever written something like that, then your addon contributes 4 tables (in this example) to garbage every time this menu is displayed. And if you've used loops to dynamically generate long menus (listing all characters' profiles, for example) then you've lost even more. For efficient use of memory you can define info once, removing all info = {} lines and adding info.checked = nil string after end of settings buttons, to ensure that checked/unchecked state of last settings button doesn't appears on action buttons. This will reduce your losses to one trashed table per call.

And, just like in previous example, you can use one permanent working table and eliminate garbage generation completely:

-- UIDropDownMenu_AddButton only needs table to conveniently pass arguments by name
-- All values are copied to their correct places inside UIDropDownMenu_AddButton
-- and it doesn't need table we passed any longer after that. This allows us to
-- reuse same table for passing parameters, since we know that reference to this
-- table won't be saved anywhere as long as we use same fields and thus overwrite
-- previous values automatically without need to clear working table in some way.

local workingTableForInitMenu={}
local function MyAddon_InitMenu()
 workingTableForInitMenu.func=nil -- erase value that could be there from last call
 
 workingTableForInitMenu.text = "Settings Button1"
 workingTableForInitMenu.value = "Some value"
 workingTableForInitMenu.checked = PlayerSettings["Setting1"]
 UIDropDownMenu_AddButton(workingTableForInitMenu)
 
 workingTableForInitMenu.text = "Settings Button2"
 workingTableForInitMenu.value = "Some other value"
 workingTableForInitMenu.checked = PlayerSettings["Setting2"]
 UIDropDownMenu_AddButton(workingTableForInitMenu)
 
 -- end of settings buttons
 
 workingTableForInitMenu.checked=nil -- erase value that could be there from last settings button
 
 workingTableForInitMenu.text = "Action Button1"
 workingTableForInitMenu.value = "Value to act on"
 workingTableForInitMenu.func = function() DoSomething() end;
 UIDropDownMenu_AddButton(workingTableForInitMenu)
 
 workingTableForInitMenu.text = "Action Button2"
 workingTableForInitMenu.value = "Another value to act on"
 workingTableForInitMenu.func = function() DoSomethingElse() end;
 UIDropDownMenu_AddButton(workingTableForInitMenu)
end

Finally, let me stress once again: this works best when you have table of same size or with same set of fields. As you see from the last example, buttons differ by two fields and function now has to take care of that. Forgetting this while using this approach can produce some very funny and hard to track bugs. And clearing table from previous values can degrade performance if there are many fields to clear on each run. Another thing to be aware of is that unlike UIDropDownMenu_AddButton, some function may store table reference and check values from it at some point in future, hoping that nobody else uses this table. When such function do that in future only to get something else that was initially passed, they tend to mess things up in quite spectacular ways. Check the target function to make sure that it doesn't store passed table.