今天和人讨论了一下CPS变形为闭包回调(典型为C#和JS),以及Lua这种具有真正堆栈,可以yield和resume的coroutine,两种以同步的形式写异步处理逻辑的解决方案的优缺点。之后突然想到,这两种做法,到底拿一种会更消耗。我自己的判断是,在一次调用只有一两个异步调用中断时(即有2次回调,或者2次yield),闭包回调的方式性能更好,因为coroutine的方式需要创建一个具有完全堆栈的协程,相对来说还是太重度了。但是如果一次调用中的异步调用非常多,那么coroutine的方式性能更好,因为不管多少次yield,coroutine始终只需要创建一次协程,而闭包回调的每一次调用都必须创建闭包函数,GC的开销不算小。直接上测试代码
CPS:
local count = 1000000 local list1 = {} local list2 = {} local clock = os.clock local insert = table.insert local remove = table.remove local function setcb(fn) insert(list1, fn) end local function test1() setcb(function() end) end local time1 = clock()--开始 for i = 1, count do test1() end local time2 = clock()--调用 while true do list1, list2 = list2, list1 for i = 1, #list2 do remove(list2)() end if #list1 == 0 then break end end local time3 = clock()--回调完全结束 print(time2 - time1, time3 - time2)
coroutine:
local count = 1000000 local list1 = {} local list2 = {} local clock = os.clock local insert = table.insert local remove = table.remove local create = coroutine.create local yield = coroutine.yield local running = coroutine.running local resume = coroutine.resume local function setcb() insert(list1, running()) yield() end local function test2() setcb() end local function test1() resume(create(test2)) end local time1 = clock()--开始 for i = 1, count do test1() end local time2 = clock()--调用 while true do list1, list2 = list2, list1 for i = 1, #list2 do resume(remove(list2)) end if #list1 == 0 then break end end local time3 = clock()--回调完全结束 print(time2 - time1, time3 - time2)
输出:
coroutine的调用和唤醒/回调,比闭包回调慢不少
(PS. 这里有个插曲,我之前设置的count = 10000000,但是测试coroutine时报内存不足的错误,因此只能下降一个数量级来测试了)
接下来我把单次调用的回调次数增多
CPS:
local count = 1000000 local list1 = {} local list2 = {} local clock = os.clock local insert = table.insert local remove = table.remove local function setcb(fn) insert(list1, fn) end local function test1() setcb(function() setcb(function() setcb(function() setcb(function() setcb(function() setcb(function() setcb(function() end) end) end) end) end) end) end) end local time1 = clock()--开始 for i = 1, count do test1() end local time2 = clock()--调用 while true do list1, list2 = list2, list1 for i = 1, #list2 do remove(list2)() end if #list1 == 0 then break end end local time3 = clock()--回调完全结束 print(time2 - time1, time3 - time2)
coroutine:
local count = 1000000 local list1 = {} local list2 = {} local clock = os.clock local insert = table.insert local remove = table.remove local create = coroutine.create local yield = coroutine.yield local running = coroutine.running local resume = coroutine.resume local function setcb() insert(list1, running()) yield() end local function test2() setcb() setcb() setcb() setcb() setcb() setcb() setcb() end local function test1() resume(create(test2)) end local time1 = clock()--开始 for i = 1, count do test1() end local time2 = clock()--调用 while true do list1, list2 = list2, list1 for i = 1, #list2 do resume(remove(list2)) end if #list1 == 0 then break end end local time3 = clock()--回调完全结束 print(time2 - time1, time3 - time2)
输出:
回调的消耗仍然是coroutine处于劣势,但已经比较接近了。启动的消耗,由于coroutine需要创建比较大的堆栈,相对于闭包来说还是比较重度,因此启动仍然远远慢于闭包回调的方式。
最后,我把一次调用里的异步接口调用次数,改成到98次(再多lua会报错:chunk has too many syntax levels),对比如下(此时次数都改成了count = 100000):
结论:闭包依然具备优势。
时间: 2024-10-01 13:57:52