今天和人討論了一下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差了太多。所以這個測試主要爲打算本身實現編程語言的讀者作爲參考。