Optimising in Lua is often counter-intuitive. Never assume a change has improved performance, even if you "know", logically, that it would have. Always check. Always.

General Optimising Tips

Root of all evil

Donald Knuth said,
" premature optimization is the root of all evil"

In other words, don't do heavy optimisation until you see things slowing down.

That is all.

Be a sniper, not a carpet bomber

Hit the target, not the square mile its in.

Much like the CPU cycles you are trying to save, your time is valuable. Don't waste it optimising code that isn't slowing you down. Profile your code, figure out where your bottlenecks are, and optimise those.

General Lua Optimisation

Keep loops short and sweet

While loops are useful and important construct in any program, they are also repetitious, and like to magnify the effects of any statements within them. Every performance tip in here applies double to loops.

When designing loops, consider if each individual statement really needs to be in the body of the loop, everything that can be legitimately moved outside the body of the loop will reduce the time it takes for the loop to complete.

Prefer numerical fors, and ipairs

Lua's tables are powerful useful and, to Lua's credit, fairly fast. The pairs iterator allows you to easily loop through all of the elements in a table, rather than ipairs merely looping through the elements with integral numeric keys in 1 to n. It is, however, not the fastest way to loop through a table, ipairs is faster, but the fastest is the humble numerical for; "for i=1,n do ... end". Yes, its not as elegant, but its still the fastest.

Use Locals

In Lua, Variables are generally either globals (the default), or locals. Lua keeps a short list of local variables that it checks before checking the global environment. Generally this means that local look-ups are "just plain faster" than globals. Normally this performance gain is splitting hairs, but there's a common situation where it can be noticeable.

Consider the following:

   for i=1,1000000 do
     localx = math.sin(i)
   local sin = math.sin
   for i=1, 1000000 do
     localx = sin(i)
  • In the WRONG WAY example, each use of math.sin() would require two look-ups, once in _G, looking for math, and again in math looking for sin; and this would happen every iteration of the loop (so about 2,000,000 look-ups).
  • In the RIGHT WAY example, we begin by creating a local containing the math.sin() function. its this value which is then used in the loop. Immediately its obvious that we've basically halved the number of look-ups required by this section code, and that's without considering that local look-ups are cheaper than global look ups.

Love-Specific Tips

Cull the herd

It can be tempting to design drawing functions that make little or no attempt to consider the visibility of game elements, relying instead on the graphics hardware's basic clipping capabilities. if nothing else, this can often give simpler and easier to understand functions.

But even if these elements aren't visible, they still need to be transformed and processed, and their data sent to the card's hardware. Depending on your game's design, you might do additional calculations for that element, such as calculating look-ats, or blending colors. All of which are unnecessary, give that the element isn't actually visible, and on non-trivial projects, this can be a performance cost you can ill-afford.

Refactor your code to avoid calculations that only need to be done visible elements, and avoid sending any data to the hardware about objects you can determine won't be visible.

Avoid Overdraw

Overdraw is when you draw over part of the screen that you had already drawn to in the current frame. much like in the Cull the Herd tip above, this is wasted effort. Why spend time drawing something that won't be seen?

The old axiom from the early days of graphical gaming was "touch every pixel only once", (I.E. zero-overdraw). fortunately hardware has come a long way since then, and some slovenliness is perfectly reasonable if it offers some other advantage (such as simpler code), but should avoid engine designs that are likely to create large amounts of overdraw.

Watch the hidden loops

Love calls the functions love.update(dt) and love.draw() frequently during the lifetime of a Love game. in fact, they are the body of loops hidden in love.run(). All the performance considerations that apply to loops, apply to these two callbacks as well.

Further Tips