效率!
警告:不要使用table.remove()。每次调用它删除数组条目时,该函数会导致所有后续(以下)数组索引重新索引。因此,我们最好在单个遍历中自己“压缩/重新索引”表格!
最佳技术很简单:通过所有数组条目向上计数(i
),同时跟踪我们应该将下一个“保留”值放入的位置(j
)。任何未保留的内容(或从i
移动到j
的内容)都设置为nil
,这告诉Lua我们已经删除了该值。
我分享这篇文章,因为我真的不喜欢本页上的其他答案(截至2018年10月)。它们要么错误,有bug,过于简单或过于复杂,并且大多数都超级慢。因此,我实现了一种高效,干净,超快速的单次遍历算法。只需一个循环。
这是一个完全注释的示例(本文末尾有更短的非教程版本):
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);
t[j] = t[i];
t[i] = nil;
else
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)
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
最后,这是可以在你自己的代码中使用的函数,没有所有的教程打印...并且只有一些最小的注释来解释最终的算法:
function ArrayRemove(t, fnKeep)
local j, n = 1, #t;
for i=1,n do
if (fnKeep(t, i, j)) then
if (i ~= j) then
t[j] = t[i];
t[i] = nil;
end
j = j + 1;
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倍。
Bonus (Advanced Users): Regular users can skip reading this bonus section. It describes how to sync multiple related tables. Note that the 3rd parameter to fnKeep(t, i, j)
, the j
, is a bonus parameter which allows your keep-function to know what index the value
will be stored at whenever fnKeep
answers true
(to keep that
value).
Example usage: Let's say you have two "linked" tables,
where one is table['Mitch'] = 1; table['Rick'] = 2;
(a hash-table
for quick array index lookups via named strings) and the other is
array[{Mitch Data...}, {Rick Data...}]
(an array with numerical indices,
where Mitch's data is at pos 1
and Rick's data is at pos 2
,
exactly as described in the hash-table). Now you decide to loop
through the array
and remove Mitch Data
, which thereby moves Rick Data
from position 2
to position 1
instead...
Your fnKeep(t, i, j)
function can then easily use the j
info to update the hash-table
pointers to ensure they always point at the correct array offsets:
local hData = {['Mitch'] = 1, ['Rick'] = 2};
local aData = {
{['name'] = 'Mitch', ['age'] = 33},
{['name'] = 'Rick', ['age'] = 45},
};
ArrayRemove(aData, function(t, i, j)
local v = t[i];
if (v['name'] == 'Rick') then
if (i ~= j) then
hData[v['name']] = j;
end
return true;
else
hData[v['name']] = nil;
return false;
end
end);
Thereby removing 'Mitch' from both the lookup hash-table and the array, and moving the 'Rick' hash-table entry to point
to 1
(that's the value of j
) where its array data is being moved
to (since i and j differed, meaning the data was being moved).
This kind of algorithm allows your related tables to stay in perfect sync,
always pointing at the correct data position thanks to the j
parameter.
It's just an advanced bonus for those who need that
feature. Most people can simply ignore the j
parameter in their
fnKeep()
functions!
好的,就这些了,朋友们,祝你们愉快! :-)
基准测试(也称“让我们开怀大笑...”)
我决定将这个算法与标准的“向后循环并使用table.remove()
”方法进行基准测试,99.9%的Lua用户都在使用这种方法。
为了进行这个测试,我使用了以下的test.lua文件:https://pastebin.com/aCAdNXVh
每个被测试的算法都有10个测试数组,每个数组包含200万个项目(每个算法测试总共2000万个项目)。所有数组中的项目都是相同的(以确保测试的完全公正):每5个项目是数字“13”(将被删除),所有其他项目都是数字“100”(将被保留)。
我的ArrayRemove()
算法测试结果为2.8秒(处理2000万个项目)。现在我正在等待table.remove()
测试完成...到目前为止已经过去了几分钟,我有点无聊......更新:还在等待...更新:我饿了...更新:今天?!更新:Zzz...更新:还在等待...更新:............更新:好吧,内置的Lua table.remove()
代码(即大多数Lua用户使用的方法)需要几天时间。我会在它完成的那一天更新。
自述:我开始在2018年11月1日GMT 04:55左右运行测试。我的ArrayRemove()
算法花费了2.8秒...内置的Lua table.remove()
算法到目前为止仍在运行...稍后我会更新这篇文章...;-)
更新:现在是2018年11月1日GMT 14:55,table.remove()
算法仍未完成。我将中止测试的这一部分,因为Lua已经占用了我的CPU100%超过10个小时,而且我现在需要使用我的电脑。而且我的笔记本电脑的铝外壳足够热可以泡咖啡了...
这是结果:
- 处理10个包含200万个项目的数组(总共2000万个项目):
- 我的
ArrayRemove()
函数:2.8秒。
- 普通Lua
table.remove()
:在Lua占用CPU 100%超过10个小时后,我决定退出测试。因为我现在需要使用我的笔记本电脑!;-)
当我按下Ctrl-C时,这是堆栈跟踪...它确认了Lua函数在过去的10个小时中一直在工作,哈哈:
[ 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万项数组中的第一项,它必须将其他2百万项的索引向下移动一个槽位以填补由删除引起的间隙。然后……当您删除另一个项目时……它必须再次移动所有其他2百万个项目……如此反复进行……
您永远不应该,
绝对不能使用
table.remove()
!其性能惩罚会迅速增加。以下是具有较小数组大小的示例,以证明这一点:
- 10个1,000项的数组(总共10,000项):
ArrayRemove()
:0.001秒,table.remove()
:0.018秒(18倍慢)。
- 10个10,000项的数组(总共100,000项):
ArrayRemove()
:0.014秒,table.remove()
:1.573秒(112.4倍慢)。
- 10个100,000项的数组(总共1m项):
ArrayRemove()
:0.142秒,table.remove()
:3分钟48秒(1605.6倍慢)。
- 10个2,000,000项的数组(总共20m项):
ArrayRemove()
:2.802秒,table.remove()
:我在10小时后决定放弃测试,所以我们可能永远不知道需要多长时间。;-) 但是,在当前时间点(甚至没有完成),它比ArrayRemove()
慢了12847.9倍……但是最终的table.remove()
结果,如果让它完成,可能会慢30-40倍。
正如你所看到的,
table.remove()
的增长并不是线性的(因为如果是线性的话,那么我们的100万项测试只会比0.1万(10万)项测试多花费10倍的时间,但实际上我们看到的是1.573秒对比3分48秒!)。因此,我们不能通过一个较低的测试(例如10k项)并将其乘以
10百万 项来知道我中途放弃的测试需要花费多长时间......因此,如果有人
真正好奇最终结果,您将需要运行测试并在几天后发布评论,当
table.remove()
完成时......
但是,我们现在可以做的是,根据我们迄今为止的基准测试,说table.remove()
很差!;-)
没有理由调用该函数。 永远都没有。因为如果您想从表中删除项目,只需使用t ['something'] = nil;
。如果您想从数组(具有数字索引的表)中删除项目,请使用ArrayRemove()
。
顺便说一下,上面的测试都是使用Lua 5.3.4
执行的,因为这是大多数人使用的标准运行时。我决定快速运行主要的“2000万项”测试,使用LuaJIT 2.0.5
(JIT:ON CMOV SSE2 SSE3 SSE4.1 fold cse dce fwd dse narrow loop abc sink fuse
),这是比标准Lua更快的运行时。使用ArrayRemove()
进行2000万个项目的结果为:Lua为2.802秒,LuaJIT为0.092秒。这意味着,如果您的代码/项目在LuaJIT上运行,则可以从我的算法中获得更快的性能!:-)
我还使用LuaJIT最后再次运行了“10万项”测试,以便我们可以看到table.remove()
在LuaJIT中的执行情况,并查看它是否比常规Lua好:
- [LUAJIT] 10个包含100,000项(总计1m项)的数组:
ArrayRemove()
:0.005秒,table.remove()
:20.783秒(比ArrayRemove()
慢4156.6倍......但是这个LuaJIT结果实际上是一个更差的比率,因为对于相同的测试,常规Lua的table.remove()
只比我的算法慢1605.6倍......因此,如果您使用LuaJIT,则性能比率更加倾向于我的算法!)
最后,你可能会想:“如果我们只想删除一个项目,
table.remove()
是否会更快,因为它是原生函数?” 如果使用LuaJIT,则该问题的答案是:否。在LuaJIT中,即使要删除一个项目,
ArrayRemove()
也比
table.remove()
更快。而且,谁不使用LuaJIT呢?对于所有Lua代码,LuaJIT可以轻松加速约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(完整的测试结果在文件末尾列出)。
总之,无论任何原因都不要在任何地方使用
table.remove()
!
希望你喜欢
ArrayRemove()
...祝大家玩得开心! :-)
table.remove()
函数应该被禁止在Lua中使用!如果您想查看基准测试,展示了table.remove()
的危险性,请向下滚动到我的标记为“效率!”的答案。亲爱的读者们:请忽略所有建议使用table.remove()
的答案。它永远不应该用于任何事情! - Mitch McMabers