效率!
警告:不要使用 table.remove()。每次调用该函数以删除数组条目时,该函数都会重新索引所有后续(后续)数组索引。因此,在单次直通 OURSELVES 中“压缩/重新索引”表要快得多!
最好的技术很简单:向上计数 (i) 通过所有数组条目,同时跟踪我们应该将下一个“保留”值放入 (j) 的位置。任何未保留的内容(或从i 移动到j)都设置为nil,这会告诉Lua 我们已经删除了该值。
我正在分享这个,因为我真的不喜欢这个页面上的其他答案(截至 2018 年 10 月)。它们要么是错误的,要么是错误的,要么是过于简单的,要么是过于复杂的,而且大多数都是超慢的。所以我实现了一个高效、干净、超快的一次性算法。使用 SINGLE 循环。
这是一个完整注释的示例(本文末尾有一个较短的非教程版本):
function ArrayShow(t)
for i=1,#t do
print('total:'..#t, 'i:'..i, 'v:'..t[i]);
end
end
function ArrayRemove(t, fnKeep)
print('before:');
ArrayShow(t);
print('---');
local j, n = 1, #t;
for i=1,n do
print('i:'..i, 'j:'..j);
if (fnKeep(t, i, j)) then
if (i ~= j) then
print('keeping:'..i, 'moving to:'..j);
-- Keep i's value, move it to j's pos.
t[j] = t[i];
t[i] = nil;
else
-- Keep i's value, already at j's pos.
print('keeping:'..i, 'already at:'..j);
end
j = j + 1;
else
t[i] = nil;
end
end
print('---');
print('after:');
ArrayShow(t);
return t;
end
local t = {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'
};
ArrayRemove(t, function(t, i, j)
-- Return true to keep the value, or false to discard it.
local v = t[i];
return (v == 'a' or v == 'b' or v == 'f' or v == 'h');
end);
输出,沿途显示其逻辑,如何移动事物等...
before:
total:9 i:1 v:a
total:9 i:2 v:b
total:9 i:3 v:c
total:9 i:4 v:d
total:9 i:5 v:e
total:9 i:6 v:f
total:9 i:7 v:g
total:9 i:8 v:h
total:9 i:9 v:i
---
i:1 j:1
keeping:1 already at:1
i:2 j:2
keeping:2 already at:2
i:3 j:3
i:4 j:3
i:5 j:3
i:6 j:3
keeping:6 moving to:3
i:7 j:4
i:8 j:4
keeping:8 moving to:4
i:9 j:5
---
after:
total:4 i:1 v:a
total:4 i:2 v:b
total:4 i:3 v:f
total:4 i:4 v:h
最后,这是在您自己的代码中使用的函数,没有所有的教程打印......并且只有几个最小的 cmets 来解释最终算法:
function ArrayRemove(t, fnKeep)
local j, n = 1, #t;
for i=1,n do
if (fnKeep(t, i, j)) then
-- Move i's kept value to j's position, if it's not already there.
if (i ~= j) then
t[j] = t[i];
t[i] = nil;
end
j = j + 1; -- Increment position of where we'll place the next kept value.
else
t[i] = nil;
end
end
return t;
end
就是这样!
如果你不想使用整个“可重用回调/函数”设计,你可以简单地将ArrayRemove()的内部代码复制到你的项目中,并将if (fnKeep(t, i, j)) then行更改为if (t[i] == 'deleteme') then ...这样你也摆脱了函数调用/回调的开销,而且速度更快!
就我个人而言,我使用可重复使用的回调系统,因为它仍然比 table.remove() 快 100-1000 倍以上。
奖励(高级用户):普通用户可以跳过阅读此奖励部分。它描述了如何同步多个相关表。请注意,fnKeep(t, i, j) 的第三个参数j 是一个额外参数,它允许您的保持函数知道该值的索引
将存储在 fnKeep 回答 true 时(以保持
值)。
示例用法:假设您有两个“链接”表,
其中一个是table['Mitch'] = 1; table['Rick'] = 2;(哈希表
通过命名字符串进行快速数组索引查找),另一个是
array[{Mitch Data...}, {Rick Data...}](带有数字索引的数组,
其中 Mitch 的数据位于 pos 1 而 Rick 的数据位于 pos 2,
完全按照哈希表中的描述)。现在你决定循环
通过array 并删除Mitch Data,从而将Rick Data 从位置2 移动到位置1...
然后您的fnKeep(t, i, j) 函数可以轻松地使用j 信息来更新哈希表
确保它们始终指向正确的数组偏移量的指针:
local hData = {['Mitch'] = 1, ['Rick'] = 2};
local aData = {
{['name'] = 'Mitch', ['age'] = 33}, -- [1]
{['name'] = 'Rick', ['age'] = 45}, -- [2]
};
ArrayRemove(aData, function(t, i, j)
local v = t[i];
if (v['name'] == 'Rick') then -- Keep "Rick".
if (i ~= j) then -- i and j differing means its data offset will be moved if kept.
hData[v['name']] = j; -- Point Rick's hash table entry at its new array location.
end
return true; -- Keep.
else
hData[v['name']] = nil; -- Delete this name from the lookup hash-table.
return false; -- Remove from array.
end
end);
从而从查找哈希表和数组中删除“Mitch”,并将“Rick”哈希表条目移动到指向
到1(这是j的值),其数组数据正在被移动
to(因为 i 和 j 不同,意味着数据正在被移动)。
这种算法可以让您的相关表保持完美同步,
由于j,始终指向正确的数据位置
参数。
对于有需要的人来说,这只是一个高级奖励
特征。大多数人可以简单地忽略他们的j 参数
fnKeep()函数!
好了,伙计们!
享受吧! :-)
基准(又名“让我们开怀大笑……”)
我决定将此算法与 99.9% 的 Lua 用户都在使用的标准“向后循环并使用 table.remove()”方法进行基准测试。
为了做这个测试,我使用了下面的 test.lua 文件:https://pastebin.com/aCAdNXVh
每个被测试的算法都有 10 个测试数组,每个数组包含 200 万个项目(每个算法测试总共包含 2000 万个项目)。所有数组中的项目都是相同的(以确保测试的公平性):每5个项目是数字“13”(将被删除),所有其他项目都是数字“100”(将被保留)。
嗯...我的ArrayRemove() 算法的测试在 2.8 秒内结束(处理 2000 万个项目)。我现在正在等待table.remove() 测试完成......到目前为止已经过了几分钟,我越来越无聊......更新:还在等待......更新:我饿了..更新:你好……今天?!更新:Zzz...更新:还在等待...更新:............更新:好的,table.remove() 代码(这是大多数 Lua 用户正在使用的方法)正在运行需要几天。我会在它完成的那一天更新。
自我说明:我于 2018 年 11 月 1 日格林威治标准时间 ~04:55 开始运行测试。我的 ArrayRemove() 算法在 2.8 秒内完成...内置 Lua table.remove() 算法仍在运行现在......我稍后会更新这篇文章......;-)
更新:现在是 2018 年 11 月 1 日格林威治标准时间 14:55,table.remove() 算法仍未完成。我将中止那部分测试,因为过去 10 小时 Lua 一直在使用我的 CPU 的 100%,我现在需要我的计算机。而且它的温度足以在笔记本电脑的铝制外壳上煮咖啡......
结果如下:
- 处理 10 个数组,包含 200 万个项目(总共 2000 万个项目):
- 我的
ArrayRemove() 函数:2.8 秒。
- 普通 Lua
table.remove():我决定在 Lua 使用 100% CPU 10 小时 后退出测试。因为我现在需要使用我的笔记本电脑! ;-)
这是我按下 Ctrl-C 时的堆栈跟踪...确认了我的 CPU 在过去 10 小时内一直在处理什么 Lua 函数,哈哈:
[ mitch] elapsed time: 2.802
^Clua: test.lua:4: interrupted!
stack traceback:
[C]: in function 'table.remove'
test.lua:4: in function 'test_tableremove'
test.lua:43: in function 'time_func'
test.lua:50: in main chunk
[C]: in ?
如果我让table.remove() 测试运行完成,可能需要几天时间...欢迎任何不介意浪费大量电力的人重新运行此测试(文件位于上方pastebin) 并让我们都知道花了多长时间。
为什么table.remove() 这么慢?仅仅是因为每个对该函数的调用都必须重复重新索引 每个存在于我们之后的表项告诉它删除!因此,要删除 200 万项数组中的第一项,它必须将所有其他 200 万项的索引向下移动 1 个插槽以填补删除造成的空白。然后......当你移除另一个项目时......它必须再次移动所有其他 200 万个项目......它一遍又一遍地这样做......
你不应该,永远使用table.remove()!它的性能损失迅速增长。下面是一个较小数组大小的示例,以证明这一点:
- 10 个包含 1,000 项的数组(总共 10k 项):
ArrayRemove():0.001 秒,table.remove():0.018 秒(慢 18 倍)。
- 10 个数组,包含 10,000 个项目(总共 100k 个项目):
ArrayRemove():0.014 秒,table.remove():1.573 秒(慢 112.4 倍)。
- 100,000 个项目的 10 个数组(总共 100 万个项目):
ArrayRemove():0.142 秒,table.remove():3 分 48 秒(慢 1605.6 倍)。
- 10 个包含 2,000,000 个项目的数组(总共 2000 万个项目):
ArrayRemove():2.802 秒,table.remove():我决定在 10 小时后中止测试,所以我们可能永远不会知道需要多长时间。 ;-) 但是在当前时间点(甚至还没有完成),它比ArrayRemove() 花费了 12847.9 倍...但是最终的table.remove() 结果,如果我让它完成,可能会在 30-40 千次左右慢一点。
如您所见,table.remove() 的时间增长不是线性的(因为如果是这样,那么我们的 100 万项测试将只花费 10 万(100k)项测试的 10 倍,但相反,我们见 1.573 秒与 3 分 48 秒!)。因此,我们不能进行较低的测试(例如 10k 个项目)并简单地将其乘以 1000 万 个项目,以知道我中止的测试将花费多长时间 ...因此,如果有人真的对最终结果感到好奇,您必须自己运行测试并在 table.remove() 完成几天后发表评论......
但是在这一点上,我们可以做的是,根据我们迄今为止的基准,说这些话:F-ck table.remove()! ;-)
没有理由调用该函数。 永远。因为如果你想从表中删除项目,只需使用t['something'] = nil;。如果要从数组(具有数字索引的表)中删除项目,请使用 ArrayRemove()。
顺便说一句,上面的测试都是使用Lua 5.3.4 执行的,因为这是大多数人使用的标准运行时。我决定使用LuaJIT 2.0.5 (JIT: ON CMOV SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse) 快速运行主要的“2000 万项”测试,它比标准 Lua 运行时更快。 ArrayRemove() 的 2000 万个项目的结果是:Lua 中为 2.802 秒,LuaJIT 中为 0.092 秒。这意味着如果您的代码/项目在 LuaJIT 上运行,您可以期待我的算法更快的性能! :-)
我还使用 LuaJIT 最后一次重新运行了“100k 个项目”测试,以便我们可以看到 table.remove() 在 LuaJIT 中的表现如何,看看它是否比常规 Lua 更好:
-
[LUAJIT] 100,000 项的 10 个数组(总共 1m 项):
ArrayRemove():0.005 秒,table.remove():20.783 秒(比 ArrayRemove() 慢 4156.6 倍...但是这个 LuaJIT 结果是实际上比普通 Lua 的 WORSE 比率,其 table.remove() 比我的算法“仅”1605.6x 在相同的测试中......所以如果你使用的是 LuaJIT ,性能比甚至更多有利于我的算法!)
最后,您可能想知道“如果我们只想删除 一个 项,table.remove() 会更快吗,因为它是一个原生函数?”。如果您使用 LuaJIT,那么该问题的答案是:不。在 LuaJIT 中,ArrayRemove() 比 table.remove() 更快,即使删除 ONE ITEM。不是 使用 LuaJIT?使用 LuaJIT,与常规 Lua 相比,所有 Lua 代码的速度都轻松提高了 30 倍左右。结果如下:[mitch] elapsed time (deleting 1 items): 0.008, [table.remove] elapsed time (deleting 1 items): 0.011。这是“仅删除 1-6 项”测试的 pastebin:https://pastebin.com/wfM7cXtU(文件末尾列出了完整的测试结果)。
TL;DR:不要使用table.remove()anywhere,出于任何任何原因!
希望大家喜欢ArrayRemove()...并玩得开心! :-)