Greetings! In this tutorial, we will touch on one of the most common operations in programming — iteration over tables (arrays) in Luau.
What is pairs
and ipairs
?
In Luau, pairs and ipairs are functions
that are used to iterate through tables. It is important to note that these functions return iterative functions
, that is, they are higher-order functions
. This means that pairs and ipairs do not iterate by themselves, but create a function that then iterates over the table.
1 pairs
: This function is designed to traverse all key-value pairs in the table. pairs can work with any type of tables (dictionaries and arrays), which makes it very flexible. However, this flexibility comes at a price — pairs
will run slower than more specialized methods.
local Table = {key1 = "value1", key2 = "value2", 10, 20}
for key, value in pairs(Table) do
print(key, value)
end
2 ipairs
: This function is designed to iterate through tables with numeric indexes starting from 1 (arrays). ipairs
returns an index-value pair
, which makes it seemingly suitable for arrays. However, as we will show, ipairs is not the fastest option.
local myArray = {10, 20, 30, 40}
for index, value in ipairs(myArray) do
print(index, value)
end
Why are they needed when you can do without them?
What is the problem with pairs
and ipairs?
As we have already found out, pairs and ipairs are functions that return iterative functions. This means that using them entails calling a function and the associated overhead.
A direct loop for _, value in array
is “nothing” (almost): When you use for _, value in array, you access the array elements directly, bypassing intermediate functions. This is not a function call, but a direct action.
Functions are always slower
Any function call, even the fastest one, takes longer than direct data access. In the case of iterating over an array, this difference may be small, but if you need maximum performance, you should pay attention to such details.
Tests
Now that we’ve discussed theory, let’s back up our claims with practice. In this section, we will conduct a series of tests to compare the performance of for _, value in array, ipairs, and pairs when iterating over arrays. We will measure the average execution time for each operation and compare the results. This will allow us to clearly see the advantages of a direct cycle
Test1: exponentiation
In this test, we will create an array of numbers and square each element at each iteration using all three iteration methods.
To begin with, we will define functions for each of the iteration methods:
Exponentiation script
local function powerIterationWithFor(array)
local result = 0
for _, value in array do
result = result + value^2
end
return result
end
local function powerIterationWithIpairs(array)
local result = 0
for _, value in ipairs(array) do
result = result + value^2
end
return result
end
local function powerIterationWithPairs(array)
local result = 0
for _, value in pairs(array) do
result = result + value^2
end
return result
end
Now, let’s create a test array and perform exponentiation:
local testArray = {}
for i = 1, 1000 do
testArray[i] = i % 10 + 1
end
local numIterations = 1000
local startFor = os.clock()
local resultFor
for i = 1, numIterations do
resultFor = powerIterationWithFor(testArray)
end
local endFor = os.clock()
local timeFor = endFor - startFor
local avgTimeFor = timeFor / numIterations
local startIpairs = os.clock()
local resultIpairs
for i = 1, numIterations do
resultIpairs = powerIterationWithIpairs(testArray)
end
local endIpairs = os.clock()
local timeIpairs = endIpairs - startIpairs
local avgTimeIpairs = timeIpairs / numIterations
local startPairs = os.clock()
local resultPairs
for i = 1, numIterations do
resultPairs = powerIterationWithPairs(testArray)
end
local endPairs = os.clock()
local timePairs = endPairs - startPairs
local avgTimePairs = timePairs / numIterations
print("No iterators:", avgTimeFor * 1000000)
print("ipairs:", avgTimeIpairs * 1000000)
print("pairs:", avgTimePairs * 1000000)
Well. I’ve done about 30 tests. I ran 100,000 iterations in each of them.
I came to the conclusion that ipairs
is about 8% slower.
Pairs
, on the other hand, turned out to be much better than my expectations. I wanted to write it off as an error
, but it almost always turned out to be just a little bit slower ~ 0.7%
Average speed:
No Iterators: 6.402 Microseconds
Ipairs: ~6.7 Microseconds
Pairs:: 6.44 Microseconds
Test 2: Concate of strings
In this test, we will create an array of strings and concatenate them into one string at each iteration using all three iteration methods. To generate strings, we will use string.char
Concate script
functions:
local function concatIterationWithFor(array)
local result = ""
for _, value in array do
result ..= value
end
return result
end
local function concatIterationWithIpairs(array)
local result = ""
for _, value in ipairs(array) do
result ..= value
end
return result
end
local function concatIterationWithPairs(array)
local result = ""
for _, value in pairs(array) do
result ..= value
end
return result
end
Now let’s create an array of strings and perform concatenation:
local testArray = {}
local numStrings = 1000
for i = 1, numStrings do
testArray[i] = string.char(math.random(65, 90))
end
local numIterations = 10000
local startFor = os.clock()
local resultFor
for i = 1, numIterations do
resultFor = concatIterationWithFor(testArray)
end
local endFor = os.clock()
local timeFor = endFor - startFor
local avgTimeFor = timeFor / numIterations
local startIpairs = os.clock()
local resultIpairs
for i = 1, numIterations do
resultIpairs = concatIterationWithIpairs(testArray)
end
local endIpairs = os.clock()
local timeIpairs = endIpairs - startIpairs
local avgTimeIpairs = timeIpairs / numIterations
local startPairs = os.clock()
local resultPairs
for i = 1, numIterations do
resultPairs = concatIterationWithPairs(testArray)
end
local endPairs = os.clock()
local timePairs = endPairs - startPairs
local avgTimePairs = timePairs / numIterations
The results surprised me very much
After calculating the average time for each method, I got the following results:
no iterators: 303.65 microseconds.
ipairs: 303.45 microseconds.
pairs: 305.98 microseconds.
ipairs turned out to be about 0.07% faster than for _, value in array
. pairs turned out to be about 0.75% slower than for _, value in array. These differences, except for pairs, are minor.
Test 3: Creating new tables
I ran only 10 tests with 1000 iterations.
In this test, we will create an array with data and create a new table at each iteration using all three iteration methods.
New tables script
First, let’s define the functions for each iteration method:
local function createTableIterationWithFor(array)
local result = {}
for _, value in array do
result[#result + 1] = {data = value}
end
return result
end
local function createTableIterationWithIpairs(array)
local result = {}
for _, value in ipairs(array) do
result[#result + 1] = {data = value}
end
return result
end
local function createTableIterationWithPairs(array)
local result = {}
for _, value in pairs(array) do
result[#result + 1] = {data = value}
end
return result
end
Now let’s create an array and create new tables:
local testArray = {}
for i = 1, 1000 do
testArray[i] = i
end
local numIterations = 1000
local startFor = os.clock()
local resultFor
for i = 1, numIterations do
resultFor = createTableIterationWithFor(testArray)
end
local endFor = os.clock()
local timeFor = endFor - startFor
local avgTimeFor = timeFor / numIterations
local startIpairs = os.clock()
local resultIpairs
for i = 1, numIterations do
resultIpairs = createTableIterationWithIpairs(testArray)
end
local endIpairs = os.clock()
local timeIpairs = endIpairs - startIpairs
local avgTimeIpairs = timeIpairs / numIterations
local startPairs = os.clock()
local resultPairs
for i = 1, numIterations do
resultPairs = createTableIterationWithPairs(testArray)
end
local endPairs = os.clock()
local timePairs = endPairs - startPairs
local avgTimePairs = timePairs / numIterations
print("no iterator:", avgTimeFor * 1000 * 1000)
print("ipairs:", avgTimeIpairs * 1000 * 1000)
print("pairs:", avgTimePairs * 1000 * 1000)
although pairs lagged behind on average, ipairs, in turn, was even a little faster in terms of speed. Although sometimes he had strange drawdowns
in speed. Maybe it’s because of my microwave.
test №6 61.58 64.55 69.30
Thus, on average, ipairs turned out to be about 0.61% slower than for _, value in array, and pairs turned out to be about 1.56% slower than for _, value in array.
As you may have noticed, the results of this test are a little more contradictory than the results of previous tests. Although pairs
proved to be the slowest on average, ipairs
was faster than for _, value in array
in several tests (I didn’t save them, though, so take it literally, lol)
However, after averaging the data, we see that for _, value in array is the most stable way to iterate over arrays, and shows the best result on average.
Conclusion
Our research has shown that although the performance difference between for _, value in array
, ipairs
and pairs
may not always be obvious, for _, value in array
is the fastest and most reliable option for iterating over arrays in Luau. I hope this tutorial was useful for you and will help you write more efficient and faster code! Thank you for your attention and have a good coding experience!”
By the way, yes. I didn’t give a damn about using the principle that you don’t have to repeat yourself in the code. Sorry