Skip to content
deltascripts.
Blog

Measure a Luau Loop Before Calling It “Optimized”

Run two equivalent loops through a repeatable timing harness, compare median measurements and understand why a microbenchmark cannot establish a whole-game FPS gain.

By deltascriptsco-admin Published 4 min read
Stopwatch beside two equal code loops for a fair performance comparison

A shorter loop isn’t necessarily a faster one. Before calling it optimized, give both versions the same work and measure them. Otherwise you may be comparing different results, or just different code styles.

The harness below runs in your own Studio project and prints its measurements. It doesn’t assume which candidate will win; the timings depend on the machine, runtime and conditions of your run.

Define a question small enough to answer

We’ll compare numeric indexing with ipairs for one dense array and repeated addition. There are no network requests, instance lookups or print calls inside the timed loops. That keeps the question small enough to answer.

Each pass adds 1 through 1000, which totals 500500. A hundred passes should return 50050000. That arithmetic gives the harness a correctness check before we start interpreting speed.

Set up a clean run

Add a Script under ServerScriptService, set RunContext to Server and disable unrelated lesson scripts. Note the Studio version, date, device and context alongside the output.

Keep the setup identical for both candidates. A server run on a desktop and a client run on another device would change far more than the loop. Repeat in the context your actual code uses before making a project decision.

Run the complete harness

local values = table.create(1000)
for i = 1, 1000 do
    values[i] = i
end

local passes = 100
local expectedTotal = 500500 * passes

local function indexed()
    local total = 0
    for _ = 1, passes do
        for i = 1, #values do
            total += values[i]
        end
    end
    return total
end

local function iterated()
    local total = 0
    for _ = 1, passes do
        for _, value in ipairs(values) do
            total += value
        end
    end
    return total
end

local function measure(fn, samples)
    local started = os.clock()
    local result = fn()
    local elapsed = os.clock() - started
    assert(result == expectedTotal, "Candidates must do equal work")
    table.insert(samples, elapsed)
end

local function median(samples)
    local ordered = table.clone(samples)
    table.sort(ordered)
    return ordered[math.ceil(#ordered / 2)]
end

assert(indexed() == expectedTotal)
assert(iterated() == expectedTotal)

local indexedSamples = {}
local iteratedSamples = {}

for round = 1, 9 do
    if round % 2 == 0 then
        measure(indexed, indexedSamples)
        measure(iterated, iteratedSamples)
    else
        measure(iterated, iteratedSamples)
        measure(indexed, indexedSamples)
    end
    task.wait()
end

print(string.format("Indexed median: %.4f ms", median(indexedSamples) * 1000))
print(string.format("ipairs median: %.4f ms", median(iteratedSamples) * 1000))
print("Expected total:", expectedTotal)

Both functions get a warm-up call. The recorded rounds alternate their order, and the code yields between pairs. Assertions and print calls sit outside the measured interval so they don’t become part of one candidate’s workload.

That helps, but it won’t remove all noise. The simulation and other background work can still affect a sample.

Read the measurements without picking a favorite

Save both medians with the environment details. If they’re close, try fresh sessions before deciding one has a repeatable advantage. Keep individual samples if you extend the harness for a closer look.

Don’t select only the fastest run from a long series. Use the same summary rule for both functions, chosen before you see the numbers.

If the interval is too short to distinguish from noise, increase the workload moderately for both candidates and rerun. Avoid a huge loop that locks up your editing session.

Distinguish throughput from frame behavior

This measures a batch of computation, not a whole frame. Rendering, physics or another script may dominate the game, leaving this loop too small to affect the FPS a player sees.

Use the Roblox MicroProfiler to find frame-level work and spikes. It helps answer whether this is the part of the program worth optimizing.

Scheduling is another question. A large synchronous batch can delay other work even when its throughput looks good. If you split work across frames, measure that design separately rather than changing it for only one candidate.

Write a defensible conclusion

When sharing the result, include the workload, context, runtime, sample count and summary method. Say whether the difference was clear or inconsistent, rather than turning one small benchmark into a rule for every Luau program.

No clear winner is a useful result too. You may decide to keep the easier loop to read and spend your time on the bottleneck the profiler actually found.

Primary references

Comments 0

No approved comments yet. Start the discussion.

Every comment is reviewed by a moderator before publication. Enter only a nickname and your comment; no email or account is needed.

Privacy policy