Skip to content
Performance: locals, OOP cost, JIT-friendliness
step 1/4

Reading — step 1 of 4

Learn

~2 min readGC, JIT, and the C Bridge

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:

  1. Lookup method in obj
  2. Fall back to metatable's __index
  3. 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 -jv or -jdump to see what's being compiled.
  • Avoid pcall, coroutine.resume, string.gsub with 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…