Revision Difference
optimizationTips#568113
<cat>Dev.Lua</cat>
<title>Concepts - Optimization Tips</title>
# 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⤶
<page text="Local Variables">Beginner_Tutorial_Variables#localvariables</page> 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.
Optimization can improve performance, but it can also make code harder to understand and maintain. Correctness and readability should usually come first.
⤶
Before optimizing, ask:⤶
- Is the code complete and working correctly?
- Is this code executed frequently enough to matter?
- Has profiling identified it as a bottleneck?
- Does the improvement justify any loss of readability?
- Will other developers need to modify this code later?
⤶
Saving a few microseconds in code that runs once is rarely worthwhile. The same saving inside `Tick`, `Think`, `HUDPaint`, rendering hooks, or a large loop may be significant because it occurs many times per second.⤶
⤶
## Measure Before Optimizing⤶
Do not assume that code is slow based only on how it looks. Measure it under realistic conditions.
⤶
<page>SysTime</page> provides sufficiently precise timing for basic benchmarks. Run a benchmark multiple times because LuaJIT compilation, current server load, and calls between Lua and the engine can affect the result.⤶
⤶
Small benchmarks are useful, but improvements should ultimately be measured in the real system where the code is used.
```lua
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⤶
local iterations = 100000⤶
local startedAt = SysTime()⤶
local result = 0⤶
⤶
for i = 1, iterations do⤶
result = result + i⤶
end⤶
⤶
local elapsed = SysTime() - startedAt⤶
⤶
-- Using result prevents the benchmarked work from being treated as unused.⤶
print( "Result:", result )⤶
print( "Total time:", elapsed )⤶
print( "Average time:", elapsed / iterations )⤶
```
⤶
### 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](https://en.wikipedia.org/wiki/Opcode) for accessing or modifying local variables⤶
- Accessing local varibles bypass any need for name resolution or [hashing](https://en.wikipedia.org/wiki/Hash_function) ⤶
⤶
Locals in [closures](https://en.wikipedia.org/wiki/Closure_(computer_programming)) are treated as [upvalues](https://www.lua.org/pil/27.3.3.html). 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.⤶
⤶
# The Most Important Tips⤶
## Use Local Variables⤶
Lua variables are global unless declared with the `local` keyword. Local variables are scoped to the current scope or function. They prevent accidental conflicts between addons while also being cheaper for the Lua VM to access.⤶
⤶
Locals are stored in function registers or captured as upvalues. Globals are retrieved from the function's environment, normally _G, using a table lookup.⤶
⤶
Use a single global addon table when information genuinely needs to be shared between files.⤶
## 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)_.
⤶
```lua⤶
--⤶
-- 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⤶
```lua⤶
someCounter = 0 -- Global: stored in the current global environment.
⤶
local someCounter = 0 -- Local: visible only within this scope.
⤶
-- When files need to share data, expose one addon table rather than many globals.
MyAddon = MyAddon or {}
MyAddon.RoundState = 0⤶
⤶
-- Keep file-internal implementation details local.⤶
local internalCounter = 0⤶
```
⤶
### 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⤶
⤶
⤶
## Avoid Repeated Work and Allocations⤶
If a value does not change, calculate or create it once and reuse it.⤶
⤶
Commonly missed cacheable tables⤶
- <page>Color</page>(-s)
- <page>Vector</page>(-s)
- <page>Angle</page>(-s)
- <page>IMaterial</page>(-s)
- <page>Entity</page>(-ies)
- <page>Player</page>(-s)
- <page>Weapon</page>(-s)
⤶
### 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.
- Results of expensive searches⤶
- Data indexed by a stable identifier⤶
⤶
Creating temporary tables such as, <page>Color</page>s, <page>Vector</page>s, or <page>Angle</page>s in frequently executed code increases allocation and garbage-collection work. <page>Color</page> and <page>Material</page>s should be cached outside rendering hooks.
⤶
Do not manually cache the result of <page>player.GetAll</page> merely to iterate over players. Garry's Mod provides <page>player.Iterator</page>, which uses an internal Lua-side cache and avoids creating a new returned table.⤶
⤶
```lua⤶
-- Unfavorable: creates objects or performs an expensive lookup every frame.⤶
hook.Add( "HUDPaint", "OptimizationExample.BadHUD", function()⤶
surface.SetMaterial( Material( "icon16/heart.png" ) )⤶
draw.SimpleText(⤶
"Healthy",⤶
"DermaDefault",⤶
32,⤶
32,⤶
Color( 100, 255, 100 ),⤶
TEXT_ALIGN_LEFT,⤶
TEXT_ALIGN_TOP⤶
)⤶
end )⤶
⤶
-- Better: create stable objects once and reuse them.⤶
local heartMaterial = Material( "icon16/heart.png" )⤶
local healthyColor = Color( 100, 255, 100 )⤶
⤶
hook.Add( "HUDPaint", "OptimizationExample.GoodHUD", function()⤶
surface.SetMaterial( heartMaterial )⤶
draw.SimpleText(⤶
"Healthy",⤶
"DermaDefault",⤶
32,⤶
32,⤶
healthyColor,⤶
TEXT_ALIGN_LEFT,⤶
TEXT_ALIGN_TOP⤶
)⤶
end )⤶
```⤶
⤶
```lua⤶
-- Unfavorable in a frequently executed hook:⤶
-- player.GetAll returns a table for us to iterate.⤶
hook.Add( "Tick", "OptimizationExample.BadPlayerIteration", function()⤶
local alivePlayers = 0⤶
⤶
for _, ply in ipairs( player.GetAll() ) do⤶
if ply:Alive() then⤶
alivePlayers = alivePlayers + 1⤶
end⤶
end⤶
⤶
DoSomethingWithCount( alivePlayers )⤶
end )⤶
⤶
-- Better:⤶
-- player.Iterator uses Garry's Mod's internally maintained player cache.⤶
hook.Add( "Tick", "OptimizationExample.GoodPlayerIteration", function()⤶
local alivePlayers = 0⤶
⤶
for _, ply in player.Iterator() do⤶
if ply:Alive() then⤶
alivePlayers = alivePlayers + 1⤶
end⤶
end⤶
⤶
DoSomethingWithCount( alivePlayers )⤶
end )⤶
```⤶
⤶
```lua⤶
-- This anonymous callback is fine. It is created once when hook.Add runs.⤶
hook.Add( "Think", "OptimizationExample.NormalClosure", function()⤶
UpdateGameState()⤶
end )⤶
⤶
-- Unfavorable: creates a table and multiple closures every tick.⤶
hook.Add( "Tick", "OptimizationExample.BadTemporaryClosures", function()⤶
local actions = {}⤶
⤶
for _, ply in player.Iterator() do⤶
actions[#actions + 1] = function()⤶
UpdatePlayer( ply )⤶
end⤶
end⤶
⤶
for i = 1, #actions do⤶
actions[i]()⤶
end⤶
end )⤶
⤶
-- Better: perform the work directly when delayed callbacks are unnecessary.⤶
hook.Add( "Tick", "OptimizationExample.GoodDirectWork", function()⤶
for _, ply in player.Iterator() do⤶
UpdatePlayer( ply )⤶
end⤶
end )⤶
⤶
```⤶
⤶
⤶
## Reuse Mutable Objects⤶
Vector arithmetic operators generally return a new Vector. When working with a Vector you own, mutating methods such as <page>Vector:Add</page>, <page>Vector:Mul</page>, and <page>Vector:Set</page> can avoid additional temporary objects.⤶
⤶
```lua⤶
local basePosition = Vector( 100, 200, 300 )⤶
local labelOffset = Vector( 0, 0, 64 )⤶
⤶
-- Creates a copy and then another Vector for the addition result.⤶
local positionWithExtraAllocation = Vector( basePosition ) + labelOffset⤶
⤶
-- Creates one copy, then modifies that copy without another addition result.⤶
local positionWithFewerAllocations = Vector( basePosition )⤶
positionWithFewerAllocations:Add( labelOffset )⤶
⤶
-- Do not do this unless you intend to permanently change basePosition:⤶
-- basePosition:Add( labelOffset )⤶
```⤶
## Using Lookup Tables Instead of Iteration⤶
⤶
## Use Lookup Tables Instead of Searching⤶
Covered more in depth on [List-Styled Tables](/gmod/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.
⤶
When a value has a known key, store it under that key.
⤶
A table lookup is approximately constant-time on average. Searching an array requires checking entries until a match is found and therefore grows linearly with the number of entries.
⤶
```lua⤶
local actionList = {⤶
{ Name = "heal", Handler = HealPlayer },⤶
{ Name = "armor", Handler = GiveArmor },⤶
{ Name = "respawn", Handler = RespawnPlayer }⤶
}⤶
⤶
-- Unfavorable: scans the array until it finds the requested action.⤶
local function RunActionBySearching( actionName, ply )⤶
for i = 1, #actionList do⤶
local action = actionList[i]⤶
⤶
if action.Name == actionName then⤶
action.Handler( ply )⤶
return true⤶
end⤶
end⤶
⤶
return false⤶
end⤶
⤶
-- Better: index handlers by the value used to find them.⤶
local actionHandlers = {⤶
heal = HealPlayer,⤶
armor = GiveArmor,⤶
respawn = RespawnPlayer⤶
}⤶
⤶
local function RunActionByLookup( actionName, ply )⤶
local handler = actionHandlers[actionName]⤶
⤶
if not handler then⤶
return false⤶
end⤶
⤶
handler( ply )⤶
return true⤶
end⤶
```⤶
⤶
## Localize Frequently Used Functions⤶
Repeated table and global lookups can be removed by storing a frequently called function in a local variable.⤶
⤶
This is most useful when the same stable function is called many times inside a hot path. There is a balance however, Localizing every function makes code harder to read and may provide no meaningful improvement.⤶
⤶
```lua⤶
local setDrawColor = surface.SetDrawColor⤶
local drawRect = surface.DrawRect⤶
⤶
hook.Add( "HUDPaint", "OptimizationExample.LocalizedFunctions", function()⤶
for i = 1, 100 do⤶
setDrawColor( 20, 20, 20, 200 )⤶
drawRect( i * 4, 20, 3, 20 )⤶
end⤶
end )⤶
⤶
-- Avoid localizing every function without evidence that it helps.⤶
-- Direct calls are clearer and normally fine:⤶
hook.Add( "HUDPaint", "OptimizationExample.DirectFunctions", function()⤶
surface.SetDrawColor( 20, 20, 20, 200 )⤶
surface.DrawRect( 20, 50, 200, 20 )⤶
end )⤶
```⤶
## 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.⤶
⤶
## Reduce Nested Loops⤶
Loops are essential to programming.
⤶
however two loops over a collection of size n perform approximately n² operations. This is quadratic growth⤶
⤶
For example:⤶
- 3 players compared with every player: 9 comparisons⤶
- 20 players compared with every player: 400 comparisons⤶
- 100 players compared with every player: 10,000 comparisons⤶
## 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:⤶
Nested loops are not always wrong. Pairwise comparisons genuinely require them in some systems. Before using one, check whether the same work can be done through:⤶
- A lookup table⤶
- A single pass⤶
- Filtering the data first⤶
- Spatial partitioning⤶
- Processing only changed objects⤶
⤶
Example:⤶
```lua
for _, ply1 in player.Iterator() do⤶
for _, ply2 in player.Iterator() do⤶
DoThingHere()
-- Unfavorable:⤶
-- For P players and E entities, this performs approximately P * E iterations.⤶
local function UpdateOwnedEntitiesNested()
for _, ply in player.Iterator() do⤶
for _, ent in ents.Iterator() do⤶
if ent:GetOwner() == ply then⤶
UpdateOwnedEntity( ply, ent )⤶
end⤶
end⤶
end⤶
end⤶
⤶
-- Better:⤶
-- Visit each entity once and retrieve its owner directly.⤶
local function UpdateOwnedEntitiesSinglePass()⤶
for _, ent in ents.Iterator() do⤶
local owner = ent:GetOwner()⤶
⤶
if IsValid( owner ) and owner:IsPlayer() then⤶
UpdateOwnedEntity( owner, ent )⤶
end⤶
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 <page text="Net Library Usage, the Improving part in particular">Net_Library_Usage#improving</page>.
⤶
⤶
⤶
# 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⤶
Network only the information that clients need, and send it only when necessary.
⤶
Prefer:⤶
- Sending changes when they occur instead of every tick⤶
- Sending to relevant recipients instead of broadcasting⤶
- Sending compact identifiers instead of entire structures⤶
- Using exact functions such as net.WriteBool, net.WriteUInt, or net.WriteEntity⤶
- Allowing clients to derive values they already have enough information to calculate⤶
⤶
<page>net.WriteTable</page> adds type and key information for each value. Writing known fields using their exact types is generally smaller and easier to validate. See <page>net.WriteTable</page> and <page>net.WriteUInt</page>.⤶
⤶
Performance is not the only networking concern. Never trust values received from a client without validating them server-side.⤶
⤶
Consider reading <page text="Net Library Usage, the Improving part in particular">Net_Library_Usage#improving</page>.⤶
⤶
```lua⤶
if SERVER then⤶
util.AddNetworkString( "OptimizationExample.RoundStateChanged" )⤶
⤶
local currentRoundState = 0⤶
⤶
local function SetRoundState( newState )⤶
-- Valid values in this example are 0 through 7.⤶
newState = math.Clamp( math.floor( newState ), 0, 7 )⤶
⤶
-- Do not send an update if nothing changed.⤶
if newState == currentRoundState then⤶
return⤶
end⤶
⤶
currentRoundState = newState⤶
⤶
net.Start( "OptimizationExample.RoundStateChanged" )⤶
net.WriteUInt( currentRoundState, 3 )⤶
net.Broadcast()⤶
end⤶
⤶
-- Call SetRoundState only when a game event changes the state.⤶
-- Do not call it from Tick merely to resend the current value.⤶
end⤶
⤶
if CLIENT then⤶
net.Receive( "OptimizationExample.RoundStateChanged", function()⤶
local newRoundState = net.ReadUInt( 3 )⤶
UpdateRoundInterface( newRoundState )⤶
end )⤶
end⤶
```
table.insert( tbl, 0 ) -- Standard⤶
⤶
tbl[#tbl + 1] = 7 -- Technically faster
⤶
# Micro-Optimizations⤶
Micro-optimizations should come after the larger issues have been addressed.
⤶
They are most appropriate when:⤶
- The system is already correct and complete⤶
- Profiling identifies the code as significant⤶
- The code runs extremely frequently⤶
- The optimized version remains maintainable⤶
⤶
### Append Directly to Sequential Arrays⤶
Assigning to `#tbl + 1` may be faster than calling <page>table.insert</page> when appending to a dense sequential array.⤶
⤶
This is only equivalent to appending. Continue using <page>table.insert</page> when inserting at a particular position or when its intent is clearer.⤶
⤶
```lua⤶
local values = {}⤶
local value = 7⤶
⤶
-- Clear and readable:⤶
table.insert( values, value )⤶
⤶
-- Potentially faster for append-only dense arrays:⤶
values[#values + 1] = value⤶
⤶
-- These are not equivalent when inserting at a specific position:⤶
table.insert( values, 1, value )⤶
```
### 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⤶
### Simplify Repeated Mathematics⤶
Some mathematically equivalent expressions require fewer operations:⤶
- Multiplication by a reciprocal may replace division.
- x * x may replace exponentiation by two.⤶
- Factoring can remove repeated multiplication.⤶
⤶
```lua⤶
local x = 10⤶
local y = 20⤶
local z = 30⤶
⤶
-- Division by a constant:⤶
local halfA = x / 2⤶
local halfB = x * 0.5⤶
⤶
-- Squaring:⤶
local squaredA = x ^ 2⤶
local squaredB = x * x⤶
⤶
-- Factoring removes one multiplication:⤶
local resultA = x * y + x * z + y * z⤶
local resultB = x * ( y + z ) + y * z⤶
⤶
-- Prefer the clearest version unless profiling demonstrates that the⤶
-- alternative provides a meaningful improvement in this exact hot path.⤶
```
# Notable Related Resources
- https://gitspartv.github.io/LuaJIT-Benchmarks/
* Focuses on the "Micro-Pico-Optimizations" thing
- https://github.com/FPtje/FProfiler
* A profiler that will help you find bottlenecks in your code
Garry's Mod
Rust
Steamworks
Wiki Help