Battle of the Loops

Stephen
5 min readMay 24, 2018

Debunking the Lua loop optimization myth & what’s really the fastest method

Edit 2020: Since the introduction of the Luau interpreter, this article is no longer relevant. See the updated Battle of the Loops: Luau Edition.

Roblox is a great platform, and many young developers start writing their first lines of code on the platform. As is the case of any newfound skill, it is easy for the newcomers to take in misinformation as truth. One in particular sticks out: a belief that using the ‘next’ function in a generic ‘for’ loop is faster than using the ‘pairs’ function, and thus is a proper optimization for a ‘for’ loop. Before I step in to tear down this idea, let me introduce myself. My name is Stephen, but most know me from my Roblox username, Crazyman32. I have been a user on Roblox for over 10 years and have four years of professional software engineering experience.

Disclaimer: I am assuming you as the reader have a general understanding of ‘for’ loops in the Lua programming language.

Generic Loops: Pairs vs. Next

Anyway, let’s get to it. Let’s examine a typical ‘for’ loop using the ‘pairs’ function. This is generally called a “generic” ‘for’ loop (as opposed to a “numeric” one, which we will discuss later):

The “pairs” function is just shorthand for returning an iterator function, which does the heavy lifting. This is executed just one time. Internally, the “pairs” function looks like this:

In essence, this is exactly the same as the “pairs” loop, since “pairs” returns the same arguments as being used in the above loop. Both methods will work exactly the same! However, many people have said that using the “next” method is faster than using the “pairs” method.

In the past, I wrote a benchmark to test both methods. However, this is pointless. When a ‘for’ loop is executed in Lua, the initial statement that sets up the loop is only executed once, not every iteration. Thus the only performance difference would be the time it takes to call the “pairs” function and return its values. The difference is minuscule to the performance of the code within the loop itself. To repeat: Once the “pairs” function is called, the loop no longer calls it again. It’s a one-and-done statement. The loop then begins and iterates with the iterator returned from the “pairs” function.

To conclude so far: Using “next” is not an optimization over “pairs” whatsoever. Anyone who uses “next” is simply bypassing calling “pairs”. Both methods are fine to use. Use whichever you prefer, but do not use one over the other because of an expected performance gain.

Numeric Loop

The “numeric” ‘for’ loop is the often-forgotten loop. The “generic” loops look and feel cleaner. To clarify: this is a totally different type of ‘for’ loop in Lua. The “pairs” and “next” methods are both “generic” loops. A “numeric” loop looks like this:

In a “numeric” loop, the loop iterates from one number to the next, incrementing the index by 1 each time (unless the loop explicitly sets the increment step to a different number). This makes it easy to loop through a table array.

The hidden secret here is that “numeric” loops are significantly faster than “generic” loops — assuming the code within the loops are the same.

Benchmark: Generic vs. Numeric Loop

To test this out, let’s set up a simple benchmark. We are going to track how long it takes for both a “generic” loop and a “numeric” loop to iterate through an array of data. For the sake of measuring startup performance of the loop, we will run the iteration through the data many times over per benchmark.

First, let’s create our data. The data itself doesn’t matter; simply the amount of data is what we care about. Let’s add 1 million items into a table:

Ok, now that we have our data, let’s create a Benchmark function that can take a function and run it x number of times, and spit out a title and the amount of time it took:

Now we just have to throw in our two different types of ‘for’ loops and test out how they perform:

When we execute all of this code in Roblox Studio, we get a result such as this:

Running Generic...
Generic duration: 5.2463755607605
Running Numeric...
Numeric duration: 4.0327010154724

In this case, the numeric loop was 0.8 seconds faster. A more meaningful measurement is the performance percentage difference. The numeric loop is roughly 30% faster. In other tests, this seemed to range between 15-40%.

Note: Benchmarking was done in the Roblox Studio environment, running Roblox’s version of Lua 5.1. Newer versions of Lua, including the LuaJIT compiler, have been known to produce different results.

Conclusion

A “numeric” loop is faster. However, in most applications, using a “generic” loop is fine. Unless you actually have performance issues, don’t worry about switching to “numeric” loops. It is looked down upon in the computer science world to optimize when you don’t need to, mainly because it makes your code unnecessarily confusing.

If you were to look at my code, you would see “generic” loops more often than “numeric” loops. I only use “numerics” when I am in need of the performance gain, or when I only care about the index.

If you need to iterate through a dictionary/hash table (i.e. a table not made up of numeric indices, but rather indexed by a hash value, such as a string), then a “generic” loop is your go-to.

Further Loop Optimizations

We simply looked at which types of loops are faster. We didn’t look at optimizing within the loop. In order to understand this, I will refer you to the grandfather PDF of Lua optimizations: the Lua Performance Tips paper, written by Roberto Ierusalimschy himself.

Quick Word on ipairs

Don’t use it. The “ipairs” function is only necessary if you need to pull numeric-indexed items from a mixed table. First of all, you should design your systems to avoid mixed tables, since it’s easy to avoid, and Lua is one of the only languages that even supports such a data structure. Secondly, “ipairs” is incredibly slow compared to “pairs”. Avoid it unless you must use it.

If you don’t know, a mixed table is when you have both hashed and numeric indices in a single table:

Again, avoid creating tables like this if you can, especially if you have to iterate through the numeric indices of the table. If you’re a Roblox game developer, then I will warn you again: mixed tables don’t play nicely. For instance, you cannot send a mixed table between the server and client via RemoteEvents or RemoteFunctions, nor can you save them in DataStores. In my opinion, it is best practice to avoid ever creating mixed data structures in your code.

--

--