效率!
警告:不要使用 table.remove()。每次调用该函数以删除数组条目时,该函数都会重新索引所有后续(后续)数组索引。因此,在单次直通 OURSELVES 中“压缩/重新索引”表要快得多!
最好的技术很简单:向上计数 ( i
) 通过所有数组条目,同时跟踪我们应该将下一个“保留”值放入 ( j
) 的位置。任何未保留的(或从i
to移动的j
)都设置为nil
告诉 Lua 我们已经删除了该值。
我正在分享这个,因为我真的不喜欢这个页面上的其他答案(截至 2018 年 10 月)。它们要么是错误的,要么是错误的,要么是过于简单的,要么是过于复杂的,而且大多数都是超慢的。所以我实现了一种高效、干净、超快速的一次性算法。使用单循环。
这是一个完整注释的示例(本文末尾有一个较短的非教程版本):
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
最后,这是在您自己的代码中使用的函数,没有所有的教程打印......并且只有一些最小的注释来解释最终算法:
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
... 这样您就可以摆脱函数调用/callback 开销也更大,而且速度更快!
就个人而言,我使用可重复使用的回调系统,因为它的速度仍然大大table.remove()
提高了 100-1000 倍以上。
奖励(高级用户):普通用户可以跳过阅读此奖励部分。它描述了如何同步多个相关表。请注意, 的第三个参数fnKeep(t, i, j)
,j
是一个奖励参数,它允许您的保持函数知道在fnKeep
回答时将值存储在哪个索引处true
(以保持该值)。
用法示例:假设您有两个“链接”表,其中一个是table['Mitch'] = 1; table['Rick'] = 2;
(通过命名字符串快速查找数组索引的哈希表),另一个是
array[{Mitch Data...}, {Rick Data...}]
(具有数字索引的数组,其中 Mitch 的数据位于 pos1
和 Rick 的数据是在 pos 2
,完全按照哈希表中的描述)。现在您决定遍历array
and remove 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
(即 的值)(因为 i 和 j 不同,表示正在移动数据)。j
这种算法允许您的相关表保持完美同步,由于j
参数,始终指向正确的数据位置。
对于需要该功能的人来说,这只是一个高级奖励。大多数人可以简单地忽略函数j
中的参数
fnKeep()
!
嗯,就是这样,伙计们!
享受!:-)
基准(又名“让我们开怀大笑……”)
我决定将此算法与table.remove()
99.9% 的 Lua 用户正在使用的标准“向后循环并使用”方法进行基准测试。
为了做这个测试,我使用了下面的 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 GMT 开始运行测试。我的ArrayRemove()
算法在 2.8 秒内完成......内置的 Luatable.remove()
算法目前仍在运行......我会更新这个稍后发布... ;-)
更新:现在是 2018 年 11 月 1 日格林威治标准时间 14:55,table.remove()
算法仍未完成。我将中止那部分测试,因为 Lua 在过去10 小时内一直在使用我 100% 的 CPU ,我现在需要我的电脑。它的热度足以在笔记本电脑的铝制外壳上煮咖啡......
结果如下:
- 处理 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 倍)。
- 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-40000 倍左右。
如您所见,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 运行时更快。2000 万个项目的结果ArrayRemove()
是: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 秒(比 ... 慢 4156.6 倍,但这个LuaJITArrayRemove()
结果实际上比普通 Lua 更糟糕,后者“仅”慢 1605.6 倍比我的算法做同样的测试...所以如果你使用的是 LuaJIT,性能比更支持我的算法!)table.remove()
最后,您可能想知道“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()
在任何地方使用!
希望你们喜欢ArrayRemove()
......并且玩得开心,大家!:-)