異步編程的兩種模型,閉包回調,和Lua的coroutine,到底哪種消耗更大

今天和人討論了一下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)

輸出:編程語言

image

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

image

回調的消耗仍然是coroutine處於劣勢,但已經比較接近了。啓動的消耗,因爲coroutine須要建立比較大的堆棧,相對於閉包來講仍是比較重度,所以啓動仍然遠遠慢於閉包回調的方式。

最後,我把一次調用裏的異步接口調用次數,改爲到10000次(須要封裝成多個函數,不然lua會報錯:chunk has too many syntax levels),對好比下(此時次數都改爲了count = 1000):

image

這個時候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)

輸出:

image

coroutine的內存佔用確實比閉包回調少不少。

所以,要內存仍是要性能,這個看本身的取捨了。

本次測試並不全面,還有不少狀況沒有測試(好比加上多個局部變量,閉包回調的性能和內存佔用可能會受影響)。而且由於lua沒有自帶的CPS變形,callback hell的存在,致使寫代碼的體驗比coroutine差了太多。所以這個測試主要爲打算本身實現編程語言的讀者作爲參考。

相關文章
相關標籤/搜索