Performance and table internals

Lua · Expert · 14 min

Hot loops in games and servers need code that works with the VM, not against it.

Internally a table has two parts: an array part for integer keys 1..n and a hash part for everything else. The array part is sized to the largest n such that more than half of the slots 1..n are in use. Growing a table triggers a rehash that recounts keys and reallocates, so building a big table one key at a time costs several reallocations; a constructor with all elements ({a, b, c}) sizes it once.

The Lua VM is register-based: locals live in registers and are the fastest variables. Upvalues are close behind. Globals are lookups in the _ENV table, and math.floor is two table lookups, which is why hot code caches them: local floor = math.floor.

Strings are interned (short ones, up to 40 bytes) and immutable. Building a string with s = s .. x in a loop is quadratic because each step copies; collect pieces in a table and call table.concat once.

Other habits:

  • Reuse tables instead of allocating in inner loops to reduce GC pressure.
  • #t on a proper sequence is cheap (a binary search at worst).
  • Measure with os.clock() before optimising; intuition is often wrong.

LuaJIT traces hot loops into machine code and can be many times faster, but it implements Lua 5.1 (plus some 5.2 features), so code must avoid 5.3+ features like integer subtypes and the // operator.

Example

local N = 200000

local t0 = os.clock()
local s = ""
for i = 1, 20000 do s = s .. "x" end      -- quadratic copying
local slow = os.clock() - t0

t0 = os.clock()
local parts = {}
for i = 1, 20000 do parts[i] = "x" end
local s2 = table.concat(parts)            -- one allocation
local fast = os.clock() - t0
print(s == s2, slow > fast)               --> true	true (usually)

local function sum_global() local x = 0
  for i = 1, N do x = x + math.sqrt(i) end return x end
local sqrt = math.sqrt
local function sum_local() local x = 0
  for i = 1, N do x = x + sqrt(i) end return x end

t0 = os.clock(); sum_global(); local g = os.clock() - t0
t0 = os.clock(); sum_local(); local l = os.clock() - t0
print(string.format("global %.4fs, local %.4fs", g, l))

Remember

  • Tables have an array part and a hash part; rehash happens on growth
  • Locals are registers: cache globals in locals in hot loops
  • Build strings with table.concat, not repeated ..

Watch out for

  • Optimising without measuring; use os.clock() or a profiler first.

Check yourself

  1. Why is building a long string with s = s .. piece inside a loop slow?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.lua.org/gems/sample.pdf

Builds on

Unlocks

Nothing builds on this yet.