今天和人讨论了一下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:lua
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:spa
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)
输出:3d
回调的消耗仍然是coroutine处于劣势,但已经比较接近了。启动的消耗,因为coroutine须要建立比较大的堆栈,相对于闭包来讲仍是比较重度,所以启动仍然远远慢于闭包回调的方式。
最后,我把一次调用里的异步接口调用次数,改为到10000次(须要封装成多个函数,不然lua会报错:chunk has too many syntax levels),对好比下(此时次数都改为了count = 1000):
这个时候coroutine的回调消耗优点就上来了。不过通常来讲,实际应用中一次调用不可能调用这么屡次异步接口。
以后再来测试内存占用
CPS:
local count = 100000 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()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function() setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function() setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function()setcb(function() end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end) end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end) end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end)end) end collectgarbage("collect") collectgarbage("stop") local count1 = collectgarbage("count") for i = 1, count do test1() end local count2 = collectgarbage("count") while true do list1, list2 = list2, list1 for i = 1, #list2 do remove(list2)() end if #list1 == 0 then break end end local count3 = collectgarbage("count") print(count2 - count1, count3 - count2, count3 - count1)
coroutine:
local count = 100000 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() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() setcb() end local function test1() resume(create(test2)) end collectgarbage("collect") collectgarbage("stop") local count1 = collectgarbage("count") for i = 1, count do test1() end local count2 = collectgarbage("count") while true do list1, list2 = list2, list1 for i = 1, #list2 do resume(remove(list2)) end if #list1 == 0 then break end end local count3 = collectgarbage("count") print(count2 - count1, count3 - count2, count3 - count1)
输出:
coroutine的内存占用确实比闭包回调少不少。
所以,要内存仍是要性能,这个看本身的取舍了。
本次测试并不全面,还有不少状况没有测试(好比加上多个局部变量,闭包回调的性能和内存占用可能会受影响)。而且由于lua没有自带的CPS变形,callback hell的存在,致使写代码的体验比coroutine差了太多。所以这个测试主要为打算本身实现编程语言的读者作为参考。