置頂隨筆
眼見為實,耳聽為虛,先上live demo:
http://luajs.org
其實現在在js環境中運行lua代碼的方案已經很多了,這些方案大都分為兩類:
VM on VM:在js環境中移植或重新實現一個Lua VM。典型的代表是lua.vm.js和MoonShineJS。 這種方案的優勢在于完整還原了整個標準lua,但主要缺陷在于,原本通過虛擬機執行的Lua VM效率就不理想,再通過JS運行,效率就更打一層折扣。
離線處理型:使用離線工具鏈將Lua語言翻譯成JS。如LLVM-Lua 加上javascript backend,還有一個名為lua2js的項目。這個方案的優勢在于能做一些較為深入的優化,缺點主要在于必須離線處理,不能直接在web上執行,或者體積龐大,以至于本身加載都需要較長時間。除此以外,只能離線處理 導致了字符串執行(如dostring等)的不可能實現,還會影響Lua中的package結構。
Lua.js采用一個不同的方案,它將lua代碼轉變為一個AST樹,經過一系列的轉換函數,最后產出一個合乎js標準的AST樹,隨后生成一個合法的js代碼。因此轉換后直接執行的代碼,比VM on VM要快2-5倍,高效的執行速度有助于讓你的應用或游戲展現流暢的極致體驗。
另外,壓縮后的Lua.js難以置信的小。這里是當前lua.js和lua.vm.js的對比:
|
文件大小 |
傳輸大小(gz壓縮) |
lua.js |
22.7K |
8.3K |
lua.vm.js |
638K |
203K |
盡管當前lua.js還有一些功能沒有實現(如metatable、Lua標準庫等),但可以預見全部實現后的尺寸也不會有大幅的增長,基本上不會超過30K/12K。
再談論性能,這里使用了lua.vm.js官方的幾個benchmark代碼(稍做修改使得可以在當前版本的lua中運行)(lua.js和lua.vm.js均在chrome下運行),結果如下:
|
lua.js |
lua.vm.js |
lua 5.2.3 |
luaJIT 2.0.3 |
Scale |
What it measures |
Binary Trees |
8.526s |
10.198s |
4.006s |
0.731s |
seconds (lower numbers are better) |
GC Performance |
Scrimark |
26.98 |
8.84 |
30.52 |
1249.73 |
MFLOPS (higher numbers are better) |
numeric computation performance |
在當前版本中,GC僅僅比lua.vm.js略快,這是因為現在lua.js對于table模擬還處于較為原型的階段,沒有經過充足的優化。但即使這樣也比lua.vm.js更快
而在數值計算上,性能遠超lua.vm.js,逼近官方lua,這同樣是在lua.js還沒有經過充足優化的前提下。經過優化,完全有超過官方Lua的可能性。
如果你對lua.js感興趣,在這里可以下載獨立的js文件:
lua.js尚處于不完善狀態,如果你決定嘗試使用,務必關注本項目的更新,及時替換更新的版本!
2014年11月6日
error(e)
Will throw a `Error` object containing the error message.
load (ld [, source [, mode [, env]]])
Only ld and env has meaning now.
dofile/loadfile, io.open, io.close, os.tmpname, os.remove, os.rename
Not supported cause there's no build-in file system supported for javascript. You can reimplement these functions on specific environment.
package.searchers, package.config, package.path, package.cpath, package.loadlib, package.searchpath
No build-in searchers for Lua files. You can reimplement searcher on specific environment. Mostly use package.preload instead.
No C loader/C lib. Use inline javascript instead.
next (table, index)
Not supported. use pairs instead.
pairs(t)
Will be supported next version, but cannot check table changes while iterating.
Will be slow if you call pairs but not iterate on it.
xpcall(f, msgh[, arg1, ...])
The stack was rewinded when msgh is called. So you cannot do anything like traceback the stack or read local variables.
string.*
There's encoding issues with lua.js now. Mostly, string.* API will use unicode instead of ANSI.
string.find, string.format, string.gmatch, string.gsub, string.match
Pattern will be supported in future. string.find with "plain = true" is usable.
os.execute, os.exit, io.popen, os.getenv
No process supported in lua.js.
os.setlocale
Not supported.
coroutine.*
The whole coroutine lib will not be supported for a long time. The browser context cannot support this feature.
debug.*
The whole debug lib is not supported now. But maybe some of them will be supported in future.
metatable:
__gc was not supported.
__mode was not supported.
2014年11月5日
眼見為實,耳聽為虛,先上live demo:
http://luajs.org
其實現在在js環境中運行lua代碼的方案已經很多了,這些方案大都分為兩類:
VM on VM:在js環境中移植或重新實現一個Lua VM。典型的代表是lua.vm.js和MoonShineJS。 這種方案的優勢在于完整還原了整個標準lua,但主要缺陷在于,原本通過虛擬機執行的Lua VM效率就不理想,再通過JS運行,效率就更打一層折扣。
離線處理型:使用離線工具鏈將Lua語言翻譯成JS。如LLVM-Lua 加上javascript backend,還有一個名為lua2js的項目。這個方案的優勢在于能做一些較為深入的優化,缺點主要在于必須離線處理,不能直接在web上執行,或者體積龐大,以至于本身加載都需要較長時間。除此以外,只能離線處理 導致了字符串執行(如dostring等)的不可能實現,還會影響Lua中的package結構。
Lua.js采用一個不同的方案,它將lua代碼轉變為一個AST樹,經過一系列的轉換函數,最后產出一個合乎js標準的AST樹,隨后生成一個合法的js代碼。因此轉換后直接執行的代碼,比VM on VM要快2-5倍,高效的執行速度有助于讓你的應用或游戲展現流暢的極致體驗。
另外,壓縮后的Lua.js難以置信的小。這里是當前lua.js和lua.vm.js的對比:
|
文件大小 |
傳輸大小(gz壓縮) |
lua.js |
22.7K |
8.3K |
lua.vm.js |
638K |
203K |
盡管當前lua.js還有一些功能沒有實現(如metatable、Lua標準庫等),但可以預見全部實現后的尺寸也不會有大幅的增長,基本上不會超過30K/12K。
再談論性能,這里使用了lua.vm.js官方的幾個benchmark代碼(稍做修改使得可以在當前版本的lua中運行)(lua.js和lua.vm.js均在chrome下運行),結果如下:
|
lua.js |
lua.vm.js |
lua 5.2.3 |
luaJIT 2.0.3 |
Scale |
What it measures |
Binary Trees |
8.526s |
10.198s |
4.006s |
0.731s |
seconds (lower numbers are better) |
GC Performance |
Scrimark |
26.98 |
8.84 |
30.52 |
1249.73 |
MFLOPS (higher numbers are better) |
numeric computation performance |
在當前版本中,GC僅僅比lua.vm.js略快,這是因為現在lua.js對于table模擬還處于較為原型的階段,沒有經過充足的優化。但即使這樣也比lua.vm.js更快
而在數值計算上,性能遠超lua.vm.js,逼近官方lua,這同樣是在lua.js還沒有經過充足優化的前提下。經過優化,完全有超過官方Lua的可能性。
如果你對lua.js感興趣,在這里可以下載獨立的js文件:
lua.js尚處于不完善狀態,如果你決定嘗試使用,務必關注本項目的更新,及時替換更新的版本!
2013年11月10日
使用最新的pomelo,新增服務器類型后出現錯誤提示:
server "account-server-1" "account" register master failed
閱讀討論帖后發現,是新版添加了adminServer.json,需要在這里添加每種服務器類型的token(默認創建的只有connector)
[
{
"type": "connector",
"token": "aga...xn"
},
{
"type": "account",
"token": "aga...xn"
}
]
添加account類型以后(token隨便填,自己生成個足夠長的字符串填進去即可)
不同服務器用相同的token也可以,取決于你的安全性要求程度。
根據官方的描述,在單機部署時,是可以沒有這個文件的,但是如果要分布式部署,必須有這個文件,當做不同服務器間(主要是master和其它服務器的monitor組件)之間通訊的驗證串來使用。
實戰pomelo過程中,自己重新進行component build之后發現輸出的網頁在瀏覽器端報錯:
Uncaught ReferenceError: Buffer is not defined 閱讀代碼后分析如下:
模塊 pomelo-protocol 的代碼,試圖兼容node.js與瀏覽器端,其區分方法是 判斷module是否是一個object。
('object' === typeof module ? module.exports : (this.Protocol = {}),'object' === typeof module ? Buffer : Uint8Array, this);
在node.js中,module是一個object,而在瀏覽器端,早期版本的component實現 把module的函數自身作為最后一個參數(命名為module)
if (!module.exports) {
module.exports = {};
module.client = module.component = true;
module.call(this, module.exports, require.relative(resolved), module);
}
所以typeof(module)得到的是一個function。
但是隨著component的更新,component改變了這個特性:
if (!module._resolving && !module.exports) {
var mod = {};
mod.exports = {};
mod.client = mod.component = true;
module._resolving = true;
module.call(this, mod.exports, require.relative(resolved), mod);
delete module._resolving;
module.exports = mod.exports;
}
可以看到最后一個參數現在是一個新創建出來的Object,所以現在在瀏覽器上,pomelo-protocol也認為現在正在node.js環境中,于是就報錯了。
一個
臨時的workaround辦法是,在require("promelo-protocol")之前,先準備好Buffer,代碼如下:
window.Buffer = Uint8Array;
var protocol = require('pomelo-protocol');
window.Protocol = protocol;
delete window.Buffer;
這樣問題暫時消除了。當然,最靠譜的辦法還是在pomelo-protocol中修改識別環境的辦法。稍后我會向pomelo提交pull-request幫助解決這一問題。
2013年8月11日
(我們)團隊里的每個人都非常重視 迅速對一知識點尋找最低成本的方式迅速付諸實踐的能力—— 我們需要知道它是做什么的,該怎么用,有何優勢與劣勢,但不需要知道每個細節。
每個不同的項目,我們都有可能迅速地接觸一兩個全新的技術并嘗試之,而不用早早的就把其中一個技術的全部細節當做自己終身的事業。
我們認為,對未知東西的迅速了解、分析能力是非常重要的,這也可以認為是我們團隊的天分所在。我們不會花太多時間去仔細研讀一個開源項目的源代碼,相反,我們迅速的閱讀完它提供的tutorial,查看社區與搜索結果,和很多已經用過它們及用過同類項目的人討論,熟悉它的風格與用法,猜測它的整體結構,思考它能做什么,不能做什么,擅長做什么,不擅長做什么,再然后,通過api reference掃描尋找我們所需要的東西。
我們崇拜并膜拜那些發明了JIT、Defered Shading的人,崇拜并膜拜boost、tbb、lfds、LuaJIT、V8、Qt、libuv、mongodb、redis等項目的貢獻者。我認為把他們(及它們)的牛逼之處在我們的產品們中充分展現是我們能為他們做的最有意義的事情。
我們會嘗試不斷的改進我們所用的每一個技術,優化細節,通過更優雅的編碼使其更具擴展性,提高性能或降低用戶犯錯的可能,毫無疑問我們要做相當多reseacher的工作。只是有一點,我們不發明技術本身。
(我們的)主要工作內容之一就是關注項目(以及其它人項目)的每一個過程,找出其中任何一個導致任何人工作緩慢或阻塞的部分,只要是通過技術手段可以改善的,我們都會通過我們的努力去解決它們。
這就是我們,Seed Engine團隊。立志以最快的速度,做最友好的游戲引擎。我們的技術未必超前,但永不落后!
和我們有相同想法的人們,歡迎你加入我們!
2013年4月12日
因為Seed Engine誕生之初,就定位為主要面向Android、iOS等移動設備(直到2012年6月才有了Flash平臺的內部版本,才開始正式需求鼠標事件),所以Seed的輸入模塊除了對鍵盤做了簡單支持(主要是出于調試目的)外,在很長的一段時間內,鼠標事件都是被wrap成為觸摸事件。所有后續關于輸入的工作都是針對觸摸來做的。所以這篇文章主要重點講觸摸這個方面,尤其是手勢識別的做法。
在Seed發展至今的期間,面臨了大量復雜的需求,不斷改進完善,目前還有很多不盡善盡美的地方。這篇文章會回顧一下這個過程,重點介紹一下當前的處理機制,再展望一下將來期望進行的改進工作。
Seed目前總計支持四種輸入:鍵盤、鼠標、觸摸、重力感應。
所有的輸入會被響應的處理模塊封裝成一個struct,這個struct的頭四個字節表明了觸摸的類型。在單線程模式下,這個struct會被直接傳遞給應用。在多線程模式下,這個struct會在堆上分配,將指針傳遞給邏輯線程。這樣的好處是在邏輯線程忙的時候,消息處理函數可以更及時返回,避免諸如窗口拖動卡頓之類的問題。
這一部分極其簡單,因為簡單,所以不容易出問題。之所以把底層架構放在歷史之前談,正是因為自從2011年10月份Seed誕生以來,引擎的這部分代碼幾乎從未變過。
對于Lua層來說,這部分的接口一直表現為一個全局事件,input.key、input.touch等等,參數struct會被iLuaWrapper(Seed中一個神秘組件)包裝成Lua可以直接訪問的數據,由Lua腳本去做任何上層的處理。
裸奔時代
Seed誕生以后的一段時間內,當時的工作重心在完善2D渲染基礎、2D場景管理,2D物理等等較為繁雜的模塊上。因為沒有具體項目的負擔,當時的輸入模塊只以滿足調試需求、實現簡單交互為目的。因此,Seed在2011年10月以前,一直處在輸入裸奔的狀態。譬如只用鍵盤做操控,那就直接注冊input.key去監聽需要的事件。為了實現諸如簡單的按鈕之類的功能,當時產生了一堆垃圾代碼,在事件里直接判定坐標等等。當然,這樣寫代碼是不可能做出沒有BUG的游戲來的,于是我們在一個游戲項目的原型階段結束后不久的時間,迅速推出了第一套框架。
山寨時代
因為需求緊迫,做任何游戲至少都少不了做一堆能tap的button出來,所以我們抓緊推出了第一版的input_ex插件。
input_ex以盡可能簡單的方式實現了對指定對象的觸摸事件處理。場景中的node可以被注冊到input_ex中,以接受tap、hold、drag事件。在touchdown的時候,input_ex會遍歷所有注冊的結點以找到被命中的node,之后的消息都會派發給這個node。
好吧,至少現在可以創建一堆不同的button了。input_ex的嚴重不足主要體現在功能上:
1、input_ex中的手寫狀態機,幾乎決定了除了tap、hold、drag以外,每加一個新的操作種類都是一個巨大的困難。
2、input_ex不能很好的處理多點觸摸。在多點的設備上各種出問題,以至于后來在某些項目里,強行屏蔽了除了1號手指(對應安卓里的0號手指)以外的所有操作。
3、消息最開始就確定了對象,隨后的過程不能改變消息的對象。譬如一個scrollview上面有一堆button,那么當button截獲了消息的時候,scrollview就無法處理相應的拖拽事件了。
除此以外,在使用上,每個節點存在一個獨立的用于處理輸入的對象,其生命周期需要手動管理,錯誤的使用會導致各種問題。初學者幾乎很難寫出正確的代碼。因為實現復雜,input_ex本身也在很長的時間內都存在引用關系的BUG,導致不需要的資源不能被正確的釋放??傮w來說,使用input_ex插件做游戲簡直是一段不堪回憶的黑歷史。
山寨時代之后
在使用input_ex完成了三四個界面操作簡單的小游戲后,我們開始構思新的輸入系統。我們理想中的輸入系統應該符合如下幾個條件:
1、很好的支持多點觸摸。這包含兩方面:第一,必須能夠很好的識別利用多個手指的操作,譬如scale, pinch, rotate等。第二,我們認為對于大屏幕的觸屏設備,能讓多個玩家在不同的地方互不干擾的進行多個操作也是很有必要的需求,這會給游戲設計師帶來很多新奇的玩法創意。
2、一定要很方便的加入各種不同的操作識別。我們希望能夠實現很多有創意的小游戲,依靠觸摸的操作來做很多有意思的事情。我們也希望我們的界面能夠交互起來更酷,可以操作控制的地方更多。那么操作一定不能只局限于區區數種,一定要在特定的游戲里就能通過代碼添加大量不同的全新操作才行。
3、操作對象的識別更智能。在scrollview 上面的button做scroll操作時,操作對象要能正確的變成scrollview。
4、根據操作對象所接受的事件有所不同,以及其父結點所接受的事件有所不同,對同一事件的處理可能會有差別。譬如一個button接受tap,當觸摸并移動的時候,只要沒移開范圍,邏輯應等待手指松開時再判定為tap成功(正如你在windows下按住一個按鈕然后小范圍拖動鼠標,click并不會因此而失敗)。但假如這個button有一個父結點甚至是祖先結點接受drag,那么早在剛開始移動的時候,就應該判定為tap取消,事件轉為drag事件而派發給相應的祖先結點。
5、不會為了滿足上面的需求,把上層邏輯代碼搞的太麻煩。最理想的情況下,上層邏輯代碼只要選擇好自己所接受的操作種類,然后安心等待事件監聽器被調用就好了。
而我們不想要:
1、像安卓那樣復雜的事件分派機制,所有的觸摸都被綁在一起依次分派下去,在結點上依據類型的不同寫代碼去做對應的操作。我們認為應該要有一個很好的手勢識別底層,僅僅把結點關心的信息拋給它。
2、像HTML DOM那樣的事件冒泡機制。因為觸摸處理的復雜性,在touch down的時候往往根本不能確定真正用戶想要進行何種操作。而如果等操作進行完了才給予反饋,那操作過程就很難得到非常及時的反饋。在上述scrollview和button的例子里,button必須首先獲得事件以立即展現被按下的效果,等到用戶的操作能夠明確為一個scroll操作之后,再由scrollview來處理后續的事件。再加上之前所述的期望4,已經不是簡單的對同一事件的冒泡足以滿足的。
真的有一套框架能完美的解決我們的需求嗎?下一章起,我會逐步講解我們為此所做的努力。
2013年2月24日
近期在做
node.js的
LuaJIT port。
LuaJIT是當前已知最快的腳本JIT編譯器,拿來做服務器再好不過。
發現node.js底層所用的庫
libuv簡直是個神器,包含了網絡、文件系統、計時器等等一堆堆的有用功能,windows、linux、MacOS等均支持,而且是純C的API,和LuaJIT結合會比較友好,理論上不用任何額外的C代碼,依靠
ffi庫就可以搞定,經過
試驗也確實如此,于此同時發現LuaJIT也真神器也,居然可以直接把Lua函數當做C函數指針傳進去當回調!正當我躊躇滿志的準備跑下性能測試就開始做上層封裝的時候,結果楞了:
1、Lua版的idle示例,等待一個idle事件被調用1e7(一千萬)次,在C下只需要區區0.1秒,在lua下需要足足30秒多!并且內存在這個過程里猛漲猛漲再猛漲,最后的gc過程耗費了更久的時間!
原版的在
這里,Lua版的在
這里。
2、嘗試添加1000次idle事件,LuaJIT直接報錯:too many callbacks
3、其他不同的嘗試均體現,性能嚴重不過關。
然后在ffi的說明里發現了
這個,提到了幾個問題:
1、callback占用某些總量有限的系統資源,所以用過的callback需要釋放,并且同時存在的callback只能有500-1000個。
2、callback函數不會被自動gc,需要用一些麻煩的辦法手動來釋放
3、callback會很慢。文中提到了類似于lua_call的消耗及argument marshalling的消耗。這點會在下面詳細講述。
總的來說,luajit里的callback,是在內存里生成了一小段代碼,這小段代碼的功能是把參數轉換好,然后再調用對應的lua函數。(還有一些奇奇怪怪的開銷,我個人認為這才是主要開銷,后面會詳細講述),因此有同時存在的總量上限(雖然我也不明白為什么就因此了,但大致就是那么回事吧),并且很慢,很慢,很,慢,很……慢……
基本上,解決方法就那么幾種:
1、做一些特定的封裝,用C額外編寫一個函數做一些處理,在這個函數里用其他方式(lua_pcall等)去調用,這樣調用參數的類型會受限一些。經測試這個只能提升50%左右(距離之前的300倍差距還差得遠……),主要是還有一些關鍵的開銷(在下面詳細講述)無法避免。
2、改寫被使用的C庫,拒絕回調,用其他辦法實現。這是LuaJIT官方所推薦的,原文如下:
For new designs avoid push-style APIs: a C function repeatedly calling a callback for each result. Instead use pull-style APIs: call a C function repeatedly to get a new result. Calls from Lua to C via the FFI are much faster than the other way round. Most well-designed libraries already use pull-style APIs (read/write, get/put).但像libuv這樣的庫,改寫難度有些大……關鍵在于重新設計整個結構為pull-style很困難,同時會導致相關文檔廢棄,增加了額外的工作量。
3、小幅度改寫使用的C庫,公開一些必須的內容,然后把其中的一部分在lua里實現,確保所有callback調用的時機均在lua中,廢棄掉原始的C API。這樣相對來說不用改變任何的接口,但是工作量也不小,取決于庫的復雜程度。
最終我在node.lua中選擇了方案3。事實證明效果確實很好,在還有一些會帶來額外開銷的功能沒加進去的情況下,之前的test優化到了0.08s左右,預計全部完成后開銷在0.15s之內,很接近純C實現的性能。
然后我又做了若干實驗,并且在freelist里和LuaJIT的創始人Mike請教了一會,得到了一些結論:
1、回調的argument marshalling是重大瓶頸之一。雖然不知道為什么,Lua對C的調用,返回值的marshalling性能很高,我推測是由于原因3。
2、把Lua-function cast成C function pointer是另一重大瓶頸,如果存在反復的類型轉換,這里會很要命。這里包含了之前所說的生成指令序列的開銷,但cast本身也會具有巨大的開銷,我嘗試將一個C function cast成 C function pointer,都帶來了極大的開銷。據Mike說,這個開銷也是原因3導致的
3、導致程序運行很慢的原因,歸根結底:某些行為會導致JIT失效!在沒有JIT的情況下,本身運行性能差不多就有幾十倍的損失,再加上一些額外開銷會因此被放大,最后就得到了不可接受的性能損失……
最后總結,目前應該在LuaJIT的ffi庫中避免使用函數指針,使用Lua本身來封裝回調函數(如果接口需要),方可獲得LuaJIT提供的卓越性能。