Reading — step 1 of 4
Learn
Lua's reference interpreter is fast. LuaJIT is much faster — sometimes within 2-3x of optimized C. To benefit from JIT (or just write fast Lua), follow these rules:
Rule 1: locals beat globals.
Globals do a hash lookup in the global table. Every. Single. Time.
-- slow:
for i = 1, 1e7 do
print(i) -- print is global; lookup each time
end
-- fast:
local print = print -- lookup once, cache as local
for i = 1, 1e7 do
print(i)
end
Do this for all hot-path globals: string.format, table.insert, math.sqrt, etc.
Rule 2: avoid allocations in inner loops.
-- BAD: allocates a table every iteration
for i = 1, 1e6 do
local t = { x = i, y = i * 2 }
process(t)
end
-- BETTER: reuse one table
local buf = { x = 0, y = 0 }
for i = 1, 1e6 do
buf.x = i
buf.y = i * 2
process(buf)
end
Closures, tables, strings (for concat), and varargs all allocate.
Rule 3: prefer ipairs over pairs when possible.
ipairs is O(1) per step (advance integer index). pairs walks the hash part — slower, especially for sparse tables.
Rule 4: table.concat beats .. in loops.
-- BAD: O(n^2) — each .. allocates a fresh string
local s = ""
for i = 1, 1000 do s = s .. tostring(i) end
-- FAST: O(n)
local parts = {}
for i = 1, 1000 do parts[i] = tostring(i) end
local s = table.concat(parts)
Rule 5: avoid OOP overhead in hot paths.
obj:method() does:
- Lookup
methodinobj - Fall back to metatable's
__index - Call as
method(obj, args...)
For 100,000 hot calls, this is real overhead. Inline or use plain functions:
local computeSpeed = Vehicle.computeSpeed
for _, v in ipairs(vehicles) do
computeSpeed(v, time)
end
Rule 6: pre-size tables when possible.
Lua 5.4: table.create(n) (or use C API lua_createtable). Pre-allocates the array part.
Rule 7: numeric for is fastest loop.
-- numeric for — JIT-friendly, no closure call
for i = 1, n do ... end
-- generic for with ipairs — calls iterator each step
for i, v in ipairs(t) do ... end
LuaJIT specifics:
- LuaJIT compiles traces (hot loops). Avoid early-exits, NYI ("not yet implemented") functions.
- Run with
-jvor-jdumpto see what's being compiled. - Avoid
pcall,coroutine.resume,string.gsubwith function replacement — these often abort traces.
Profiling Lua:
local start = os.clock()
run_code()
print(string.format("took %.4fs", os.clock() - start))
For deeper profiling: LuaProfiler, lua-statsd-client, or LuaJIT's -jp option.
The 80/20: cache globals as locals + avoid unnecessary allocations gets you most of the wins. Don't optimize prematurely; profile first.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…