Garry's Mod Wiki

Concepts - Optimization Tips

Knowing When to Optimize

While proper optimization ups the code's performance, at times it may disperse the code's readability. If you or others are actively working on some code, maintaining the code's readability is more preferable first for shorter development time.

Knowing in which state you want your code to be should be the first thing on the mind before taking on optimizing.

  • Is the code a final complete solution?
  • Will anyone ever wanna look at this code to figure out how it works?
  • Does saving 3 or so microseconds of CPU time justify potential consequent illegibility of the code?

This article introduces some tips that are technically faster but harder to interpret at a glance. Before you implement them at the sake of speed consider if compromising the legibility of this section of code is excusable.

That being said, there are places where you should prioritize optimization: anything that runs regularly (e.g. code inside those hooks/functions called every tick or frame) may impact performance and should be one of the first places you consider optimizing.

The Most Important Tips

These tips are broadly applicable and more often than not should be the default. Each subheading will provide the basic version of the tip and then a deeper understanding of its rationale.

Use Local Variables

Local Variables are noticeably faster than global variables. This tip is so broadly applicable that the standard should be to always use local variables unless there is no way to achieve the desired result without using a local.

someRegularVar = 0 -- Unfavorable. Lua is global by default local someRegularVar = 0 -- Fine. 'someRegularVar' will only be accessible within the scope in which it's defined

Understanding Why

This difference comes from how the Lua Virtual Machine handles them internally. Local variables are compiled into registers, these are fixed-size slots allocated on the stack for each function. During runtime, each local variable is assigned an integer index (the offset) into this register array.

This means:

  • Access to local variables now only need a direct index operation
  • The Lua VM emits a single opcode for accessing or modifying local variables
  • Accessing local varibles bypass any need for name resolution or hashing

Locals in closures are treated as upvalues. These upvalues point directly to the stack (or heap allocated closure variable) if the function outlives the scope) maintaining its fast access.

Globals on the other hand, are not stored in fixed memory slots. Instead, they are entries into a special table. For Garrys Mod (Lua version 5.1) this is the _G table.

The process to access a global variable from that table involves:

  1. Hashing the variable string name
  2. Performing a table lookup using that key
  3. Returning or modifying the value associated with that key.

This where the next broadly applicable tip comes in super handy.

Caching

Try to always cache repeated lookups of the same data.

If the information is persistent, there's no need to rebuild the entire table each time. Instead, build the table once and reuse it.

If the information changes over time, you can monitor changes related to the stored data and then rebuild/update the table when necessary.

This is crucial in performance-weighty areas of your code (notable examples are hooks like Tick, Think, and Render-related).

-- -- Bad Example -- hook.Add( "Tick", "*BadExample*", function() for i, ply in ipairs( player.GetAll() ) do -- New table is created each tick and returned from C to Lua, thanks to the player.GetAll() print( ply:GetName() ) end end ) -- -- Good Example -- local cachedPlayers = {} local function updateCache() cachedPlayers = player.GetAll() end hook.Add( "PlayerConnect", "PlayerJoinedUpdateCache", updateCache ) hook.Add( "PlayerDisconnected", "PlayerLeftUpdateCache", updateCache ) local function DoStuff() for i, ply in ipairs( cachedPlayers ) do -- Now we we have one table that we can reuse, instead of rebuilding a new one each time print( ply:GetName() ) end end hook.Add( "Tick", "*GoodExample*", DoStuff ) -- Notice the two things we've improved here, avoiding creating repeated anonymous functions (closures) and using a cached table

What to cache?

A thing to remember is object creation. If calling a function or completing an operation results in an object being created, you want to do that as few times as possible.

Common Things Not Cached that better be cached

Understanding Why

Creating a new object implies memory allocation, initializing the object, and, commonly, constructing the object. That potentially triggers garbage collection, which may produce a lag, and increases CPU time.

Also, if supported, using arithmetic operators upon these objects produces a new object with the operation's result. If possible, replace with the corresponding metamethods, which will modify the original object instead.

Using Lookup Tables Instead of Iteration

Covered more in depth on List-Styled Tables.

Looking up a value in a table by known key is much faster than iterating through the entire table to find the needed value.

value = tbl["knownkey"] > searching value through for-loop.

Localizing Common & Meta Functions

Some coding applications run some set of functions a lot. When this is unavoidable it'll be resourceful to pick out most expensive and/or most frequently called functions and localize them.

Balance here is a key. Localizing every function will be redundant and at times even counterproductive. Use this technique sparsely.

Avoid Nested Loops (Exponential Growth)

Loops are essential to programming.

But Nested Loops should, as a rule, be avoided at all if possible. This is because of exponential growth of the number of operations. Example:

for _, ply1 in player.Iterator() do for _, ply2 in player.Iterator() do DoThingHere() end end

If we do some arithmetic, for 3 players this code will run only 9 times (3 * 3), but for 20 players this will run all the 400 times (20 * 20).

Minimize Networking

Consider reading Net Library Usage, the Improving part in particular.

Micro-Pico-Optimizations

This is about squeezing out the capacity for high performance as maximally as achievable.

You'll never have a dire need in these techniques. This is quite suitable when the product is in a final state and you want to fine-tune it in terms of performance; or, you know what you're doing and that's your preference. But by and large it's preferable to save yourself time and not integrate these everywhere and always.

Using Inline Expression over Function Calls

table.insert( tbl, 0 ) -- Standard tbl[#tbl + 1] = 7 -- Technically faster

Math

Computers can solve certain math equations quicker, restructuring math equations to increase its calculation's efficiency.

This may also make your math harder to follow. In such a scenario, outlining your math in the code would be relevant.

-- -- Multiplication over Division -- x / 2 -- Standard x * 0.5 -- Technically faster -- -- Squaring over Exponentiation -- x ^ 2 -- Standard x * x -- Technically faster -- -- Factoring Expressions -- x * y + x * z + y * z -- Standard x * ( y + z ) + y * z -- Technically faster

Notable Related Resources