使用数字索引遍历数组/table 的最快方法是什么?

What is the fastest way to go through an array / table with numeric indices?

如果我在 lua 中有一个带有编号索引的数组,并且需要至少遍历每个条目一次,使用数字 for 循环还是通用 for 循环更快?

在这种情况下,使用数字 for 循环会更快。但差别不大,在我的测试中,它更容易在我的系统上出现负载差异。

数字循环

a = {} -- new array
for i = 1, 10000000 do
    a[i] = 10000000 + i
end

local startNumLoop = os.time(os.date("!*t"))

for i = 1, #a, 1 do
    local value = a[i]
end

local stopNumLoop = os.time(os.date("!*t"))

local numloop = stopNumLoop - startNumLoop 

print(os.clock())

结果:1.379 - 1.499

通用循环

a = {} -- new array
for i = 1, 10000000 do
    a[i] = 10000000 + i
end

local startGenLoop = os.time(os.date("!*t"))

for index, value in ipairs(a) do

end

local stopGenLoop = os.time(os.date("!*t"))

local genLoop = stopGenLoop- startGenLoop

print(os.clock())

结果:1.568 - 1.662

使用来自 lua.org

的 lua 5.3.6 win32 二进制文件进行测试

这只是我的一个问题,我没有足够快地找到答案。如果它实际上是重复的,请随意标记它。 :)

语义差异

for i = 1, #t do end

不等于

for i, v in ipairs(t) do end

后者不依赖于 #t 并尊重 __ipairs 元方法(尽管这已被弃用,应该使用 __index__newindex 元方法代替)。

假设没有设置元方法,ipairs 将简单地循环直到它遇到一个 nil 值。因此它大致等同于以下循环:

local i, v = 1, t[v]
while v ~= nil do --[[loop body here]] i = i + 1; v = t[i] end

这意味着它不必做两件事:

  • 不决定长度。如果设置,它不会调用 __len 元方法。这在理论上可能会导致位于散列部分的列表具有更好的性能(其中 Lua 必须通过搜索确定长度)。它还可以在 __len 元方法进行昂贵计数的情况下提高性能。
  • 它不必遍历 nil 个值。另一方面,由于长度运算符的定义方式,数字 for 循环可能会遍历任意多个 nil 值:对于 table {[1] = 1, [1e9] = 1}11e9 是长度的有效值。这也意味着不清楚它的作用,因为未指定确切的长度值。

后一点特别意味着在病态情况下,数字 for 循环可能任意慢。它还允许错误,例如循环(可能很长)字符串而不是 tables,并且不会触发错误:

local str = "hello world"
for i = 1, #str do local v = str[i] end

将仅遍历 nil 值(因为它索引字符串元 table)但不会抛出错误。

我还认为 ipairs 更具可读性,因为它使意图清晰。

性能差异

对于 non-pathological 情况(列表位于列表部分,没有“空洞”(零值),没有奇元 tables),数字 for 循环可以预期 运行 稍快一些,因为它不会产生您将与 ipairs 一起使用的通用循环的调用开销。这应该在不同的 Lua 实现上进行基准测试:

  • PUC Lua 5.1 到 5.4
  • LuaJIT 2.1.0

实际上,与在循环体内执行的操作的成本相比,循环的成本通常可以忽略不计。结果可能因操作系统或硬件等其他因素而异。

基本基准

print(jit and jit.version or _VERSION)

-- Fill list with 100 million consecutive integer values
a = {}
for i = 1, 1e8 do a[i] = i end

local function bench(name, func)
    func() -- "warmup"
    local t = os.clock()
    for _ = 1, 10 do func() end
    print(name, os.clock() - t, "s")
end

bench("numeric", function()
    for i = 1, #a do
        local v = a[i]
    end
end)

bench("ipairs", function()
    for i, v in ipairs(a) do end
end)

在 Linux 机器上进行。

Lua 5.1
numeric 54.312082   s
ipairs  63.579478   s
Lua 5.2
numeric 20.482682   s
ipairs  32.757554   s
Lua 5.3
numeric 14.81573    s
ipairs  23.121844   s
Lua 5.4
numeric 11.684143   s
ipairs  24.455616   s

最后,LuaJIT:

LuaJIT 2.1.0-beta3
numeric 0.567874    s
ipairs  0.70047 s

结论:尽可能使用 LuaJIT,不再担心 micro-optimizations,例如 ipairs 与数字 (即使后者可能稍快)。像 assert(i == v) 这样简单的东西已经和循环本身一样昂贵了(即使 assertlocal)。

特别是对于需要 remove/change 当前 key/value 对的循环,我更喜欢快速倒计时方法...

for i = #tab, 1, -1 do
 if (tab[i].x > x) then
  table.remove(tab, i) -- See comments
 end
end

...例如在 LÖVE [love2d] 中两帧之间。