由eval生成的代碼效率真的很差嗎?

已邀請:

大叔遇上狼

贊同來自:


昨晚跟一位Node.js專傢講解瞭我的Wind.js類庫。之前那位仁兄對Jscex(Wind.js的前身)的看法是“就是不喜歡”,也在微博上對Jscex冷嘲熱諷,於是我私信他說建議看一下文檔瞭解一下Jscex。昨天我們的爭論主要圍繞在eval的使用上,他認為更好的做法是像CoffeeScript那樣使用一個額外的進程監聽改變,這樣更方便。我說CoffeeScript這麼做是因為它沒有像Wind.js那樣借助eval實現完全動態的運行時轉化,且生產環境中不會出現eval。最後他堅持認為“eval就是有性能問題”,因此開發時也不應該使用,否則Wind.js為什麼要提供預編譯器?雖然最後不歡而散,不過我忽然也打算驗證一下eval生成的代碼效率到底會差到什麼樣的地步,於是便有瞭這次試驗。


測試代碼


有人可能會問,eval每次動態的執行代碼時需要重新分析代碼,還不能進行優化,為什麼會“不慢”?不過請註意,這裡測試的目標不是用eval執行代碼慢不慢,而是反復執行用eval生成的代碼,這才是Wind.js對eval的使用方式。Wind.js中每次eval將會生成一個函數,然後在使用的過程中不會反復eval


既然是測試純性能,我就找瞭個純粹用來計算的函數:LCG隨機數生成器。這個隨機數生成器實現簡單,效率極高,因此運用十分廣泛,這裡不討論它的原理,隻給出它的JavaScript實現:



var nativeRandomLCG = function (seed) {
return function () {
seed = (214013 * seed + 2531011) % 0x100000000;
return seed * (1.0 / 4294967296.0);
};
};

var evalRandomLCG = function (seed) {
var randomLCG = eval("(" + nativeRandomLCG.toString() + ")");
return randomLCG(seed);
};

nativeRandomLCG將通過seed生成一個隨機數生成器,而evalRandomLCG則會將前者的代碼作為字符串取出,並在eval後重新獲得隨機數生成器。我們的評測對象便是這兩個生成器:



var nativeSuite = {
name: "native",
target: nativeRandomLCG(100)
};

var evalSuite = {
name: "eval",
target: evalRandomLCG(100)
};

var iterations = [100, 200, 300];

for (var round = 0; round < iterations.length; round++) {
console.log("Round " + round);
test(iterations[round] * 1000 * 1000, nativeSuite, evalSuite);
console.log("");
}

我將進行三輪測試,分別生成100M,200M及300M個隨機數,並觀察兩個隨機數生成器的耗時。完整代碼可以在此獲得,其中lcg.js可以直接作為Node.js程序運行,而lcg.html則可以在瀏覽器裡打開,點擊頁面上的按鈕啟動計算,測試結果會在控制臺裡輸出。


由於Node.js可以在不同平臺上使用,而Windows和OSX又各有一個不跨平臺的瀏覽器,因此我會在兩個平臺上分別對Node.js和主流瀏覽器進行測試。為瞭避免Firefox出現“腳本執行時間過長,是否中止”這樣的提示,還需要對其進行簡單的設置。


Windows實驗結果


Windows上的實驗使用的是我工作時所使用的ThinkPad 520,操作系統為Windows 7,CPU信息如下:



四個JavaScript執行環境為:



  1. Node.js 0.8.6

  2. IE 9

  3. Chrome 21

  4. Firefox 14


實驗結果:


圖表:


從中我們可以清晰地得出:


對於Node.js來說,使用eval得到的函數,其執行效率和JavaScript直接定義的函數可謂毫無二至。事實上,除瞭IE似乎eval普遍稍慢於原生函數外,其他引擎裡的表現都可謂基本持平,連IE的落後也基本可以忽略不計。所以,“eval出來的代碼效率會有很大差距”這種說法,至少在這個實驗中絲毫沒有體現出來。


有意思的是,在Chrome中還發生瞭這麼一件事情:在第一輪比較中,原生定義的函數有較大的性能優勢,而且自從測試瞭eval得到的函數之後,後面兩輪連原生函數的效率都下降瞭。這會不會是因為eval讓整個執行引擎大打折扣瞭呢?嚴謹起見,我又測試瞭三種情況:



  • 兩個測試用例都使用原生函數。

  • eval得到的函數先於原生函數前執行。

  • 兩個測試都使用eval得到的函數。


都得到瞭類似的結果:先執行的函數效率會高出許多。我猜測這是因為Chrome發現瞭大量的密集計算,為瞭保證界面的響應能力,將JavaScript執行的優先級降低的緣故。值得一提的是,盡管Chrome的“降速”後的結果略慢於IE和Firefox,但它是唯一一個在性能測試的時候,整個界面還沒有失去響應的瀏覽器。此時我可以切換到其他Tab,也可以關閉這張頁面——甚至控制臺裡輸出的文字也是立即出現的,而其他兩個瀏覽器都必須等整段程序執行完成之後所有輸出才同時出現。


必須承認,Chrome瀏覽器在這方面的確可圈可點。


OSX實驗結果


OSX上的實驗使用的是一臺高配的MBP with Retina Display,操作系統為最新的OSX Mountain Lion,CPU信息如下:



四個JavaScript執行環境為:



  1. Node.js 0.8.0

  2. Safari 6

  3. Chrome 21

  4. Firefox 14


實驗結果:


圖表:


由於結論十分類似,我就不多做分析瞭。不過有意思的是,不知為何Safari瀏覽器的性能極低,我一開始使用相同規模的實驗數據,發現Safari遲遲不返回結果,直到我將生成隨機數的數量降低到十分之一時,才得到Safari如今的耗時。因此請註意這裡的Safari附帶的x0.1字樣,正是指它實驗數據的規模僅僅為其他JavaScript執行引擎的十分之一。


總結


eval本身的執行無疑會慢,因為它需要動態的分析那段字符串的內容才能執行,且單次執行為它進行優化可能也得不償失。但是,像Wind.js那樣將eval的結果反復執行,並非反復執行eval本身,這可能就是另外一回事情瞭。eval最終還是得到一段代碼,而這段代碼在反復執行過程中可能也會被JIT,也會被優化。


我不能說我設計的這個簡單案例就能說明一切,但是至少通過這個我立即就想到的首個測試用例,能夠說明eval的某些使用方式可能並不想許多人幻想中對性能會產生多大的影響。而且對於Wind.js來說,如果你實在害怕eval,完全可以在生產環境中通過預編譯消除所有的eval。這裡的預編譯並不是為瞭性能,而是讓代碼可以脫離體積最大的編譯器模塊運行,剩下的部分在Minified和GZipped之後隻有4K大小。


如果你非要如那位Node.js專傢說“eval就是慢”,那我隻能這麼說:


在Wind.js中使用eval不會產生什麼性能問題,我覺得排斥的人有三種情況:不熟悉eval,不熟悉是不瞭解Wind.js,找茬。


我在微博上說瞭上面這句話之後,卻得到那位專傢這樣的回應:


看來,不喜歡Wind.js的人要麼是不懂,[email protected] :我覺得排斥的人有三種情況:不熟悉eval,不熟悉Wind.js,找茬……


我頓時覺得很生氣,說道:


身為有頭有臉的專傢不要斷章取義,我說的是排斥“Wind.js裡使用eval”。你可以不喜歡Wind.js,但是要說出靠譜的理由。上次你也跑上來黑一票Wind.js就走,真枉我買過你的書。


結果卻得到對方正義地批評:


還是請您就事論事,不要人身攻擊。


真令人好生疑惑,我這就算是人身攻擊?哪裡沒有就事論事?是誰在斷章取義,搞低級黑?


那攻擊就攻擊吧,反正我已經很失望瞭。

要回復問題請先登錄註冊