Concepts - Optimization Tips
Knowing When to Optimize
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.
SysTime 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.
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.
Avoid Repeated Work and Allocations
If a value does not change, calculate or create it once and reuse it.
Commonly missed cacheable tables
- Color(-s)
- Vector(-s)
- Angle(-s)
- IMaterial(-s)
- Entity(-ies)
- Player(-s)
- Weapon(-s)
- Results of expensive searches
- Data indexed by a stable identifier
Creating temporary tables such as, Colors, Vectors, or Angles in frequently executed code increases allocation and garbage-collection work. Color and Materials should be cached outside rendering hooks.
Do not manually cache the result of player.GetAll merely to iterate over players. Garry's Mod provides player.Iterator, which uses an internal Lua-side cache and avoids creating a new returned table.
Reuse Mutable Objects
Vector arithmetic operators generally return a new Vector. When working with a Vector you own, mutating methods such as Vector:Add, Vector:Mul, and Vector:Set can avoid additional temporary objects.
Use Lookup Tables Instead of Searching
Covered more in depth on List-Styled Tables.
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.
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.
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
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:
Minimize Networking
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
net.WriteTable adds type and key information for each value. Writing known fields using their exact types is generally smaller and easier to validate. See net.WriteTable and net.WriteUInt.
Performance is not the only networking concern. Never trust values received from a client without validating them server-side.
Consider reading Net Library Usage, the Improving part in particular.
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 table.insert when appending to a dense sequential array.
This is only equivalent to appending. Continue using table.insert when inserting at a particular position or when its intent is clearer.
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.
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