感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運大獎:點擊這里領(lǐng)取
本書的前四章都是關(guān)于代碼模式(異步與同步)的性能,而第五章是關(guān)于宏觀的程序結(jié)構(gòu)層面的性能,本章從微觀層面繼續(xù)性能的話題,關(guān)注的焦點在一個表達(dá)式/語句上。
好奇心的一個最常見的領(lǐng)域——確實,一些開發(fā)者十分癡迷于此——是分析和測試如何寫一行或一塊兒代碼的各種選項,看哪一個更快。
我們將會看到這些問題中的一些,但重要的是要理解從最開始這一章就 不是 為了滿足對微性能調(diào)優(yōu)的癡迷,比如某種給定的JS引擎運行++a
是否要比運行a++
快。這一章更重要的目標(biāo)是,搞清楚哪種JS性能要緊而哪種不要緊,和如何指出這種不同。
但在我們達(dá)到目的之前,我們需要探索一下如何最準(zhǔn)確和最可靠地測試JS性能,因為有太多的誤解和謎題充斥著我們集體主義崇拜的知識庫。我們需要將這些垃圾篩出去以便找到清晰的答案。
基準(zhǔn)分析(Benchmarking)
好了,是時候開始消除一些誤解了。我敢打賭,最廣大的JS開發(fā)者們,如果被問到如何測量一個特定操作的速度(執(zhí)行時間),將會一頭扎進(jìn)這樣的東西:
var start = (new Date()).getTime(); // 或者`Date.now()`
// 做一些操作
var end = (new Date()).getTime();
console.log( "Duration:", (end - start) );
如果這大致就是你想到的,請舉手。是的,我就知道你會這么想。這個方式有許多錯誤,但是別難過;我們都這么干過。
這種測量到底告訴了你什么?對于當(dāng)前的操作的執(zhí)行時間來說,理解它告訴了你什么和沒告訴你什么是學(xué)習(xí)如何正確測量JavaScript的性能的關(guān)鍵。
如果持續(xù)的時間報告為0
,你也許會試圖認(rèn)為它花的時間少于1毫秒。但是這不是非常準(zhǔn)確。一些平臺不能精確到毫秒,反而是在更大的時間單位上更新計時器。舉個例子,老版本的windows(IE也是如此)只有15毫秒的精確度,這意味著要得到與0
不同的報告,操作就必須至少要花這么長時間!
另外,不管被報告的持續(xù)時間是多少,你唯一真實知道的是,操作在當(dāng)前這一次運行中大概花了這么長時間。你幾乎沒有信心說它將總是以這個速度運行。你不知道引擎或系統(tǒng)是否在就在那個確切的時刻進(jìn)行了干擾,而在其他的時候這個操作可能會運行的快一些。
要是持續(xù)的時間報告為4
呢?你確信它花了大概4毫秒?不,它可能沒花那么長時間,而且在取得start
或end
時間戳?xí)r會有一些其他的延遲。
更麻煩的是,你也不知道這個操作測試所在的環(huán)境是不是過于優(yōu)化了。這樣的情況是有可能的:JS引擎找到了一個辦法來優(yōu)化你的測試用例,但是在更真實的程序中這樣的優(yōu)化將會被稀釋或者根本不可能,如此這個操作將會比你測試時運行的慢。
那么...我們知道什么?不幸的是,在這種狀態(tài)下,我們幾乎什么都不知道。 可信度如此低的東西甚至不夠你建立自己的判斷。你的“基準(zhǔn)分析”基本沒用。更糟的是,它隱含的這種不成立的可信度很危險,不僅是對你,而且對其他人也一樣:認(rèn)為導(dǎo)致這些結(jié)果的條件不重要。
重復(fù)
“好的,”你說,“在它周圍放一個循環(huán),讓整個測試需要的時間長一些。”如果你重復(fù)一個操作100次,而整個循環(huán)在報告上說總共花了137ms,那么你可以除以100并得到每次操作平均持續(xù)時間1.37ms,對吧?
其實,不確切。
對于你打算在你的整個應(yīng)用程序范圍內(nèi)推廣的操作的性能,僅靠一個直白的數(shù)據(jù)上的平均做出判斷絕對是不夠的。在一百次迭代中,即使是幾個極端值(或高或低)就可以歪曲平均值,而后當(dāng)你反復(fù)實施這個結(jié)論時,你就更進(jìn)一步擴(kuò)大了這種歪曲。
與僅僅運行固定次數(shù)的迭代不同,你可以選擇將測試的循環(huán)運行一個特定長的時間。那可能更可靠,但是你如何決定運行多長時間?你可能會猜它應(yīng)該是你的操作運行一次所需時間的倍數(shù)。錯。
實際上,循環(huán)持續(xù)的時間應(yīng)當(dāng)基于你使用的計時器的精度,具體地將不精確的 ·可能性最小化。你的計時器精度越低,你就需要運行更長時間來確保你將錯誤的概率最小化了。一個15ms的計時器對于精確的基準(zhǔn)分析來說太差勁兒了;為了把它的不確定性(也就是“錯誤率”)最小化到低于1%,你需要將測試的迭代循環(huán)運行750ms。一個1ms的計時器只需要一個循環(huán)運行50ms就可以得到相同的可信度。
但,這只是一個樣本。為了確信你排除了歪曲結(jié)果的因素,你將會想要許多樣本來求平均值。你還會想要明白最差的樣本有多慢,最佳的樣本有多快,最差與最佳的情況相差多少等等。你想知道的不僅是一個數(shù)字告訴你某個東西跑的多塊,而且還需要一個關(guān)于這個數(shù)字有多可信的量化表達(dá)。
另外,你可能想要組合這些不同的技術(shù)(還有其他的),以便于你可以在所有這些可能的方式中找到最佳的平衡。
這一切只不過是開始所需的最低限度的認(rèn)識。如果你曾經(jīng)使用比我剛才幾句話帶過的東西更不嚴(yán)謹(jǐn)?shù)姆绞竭M(jìn)行基準(zhǔn)分析,那么...“你不懂:正確的基準(zhǔn)分析”。
Benchmark.js
任何有用而且可靠的基準(zhǔn)分析應(yīng)當(dāng)基于統(tǒng)計學(xué)上的實踐。我不是要在這里寫一章統(tǒng)計學(xué),所以我會帶過一些名詞:標(biāo)準(zhǔn)差,方差,誤差邊際。如果你不知道這些名詞意味著什么——我在大學(xué)上過統(tǒng)計學(xué)課程,而我依然對他們有點兒暈——那么實際上你沒有資格去寫你自己的基準(zhǔn)分析邏輯。
幸運的是,一些像John-David Dalton和Mathias Bynens這樣的聰明家伙明白這些概念,并且寫了一個統(tǒng)計學(xué)上的基準(zhǔn)分析工具,稱為Benchmark.js(http://benchmarkjs.com/)。所以我可以簡單地說:“用這個工具就行了。”來終結(jié)這個懸念。
我不會重復(fù)他們的整個文檔來講解Benchmark.js如何工作;他們有很棒的API文檔(http://benchmarkjs.com/docs)你可以閱讀。另外這里還有一些了不起的文章(http://calendar.perfplanet.com/2010/bulletproof-javascript-benchmarks/)(http://monsur.hossa.in/2012/12/11/benchmarkjs.html)講解細(xì)節(jié)與方法學(xué)。
但是為了快速演示一下,這是你如何用Benchmark.js來運行一個快速的性能測試:
function foo() {
// 需要測試的操作
}
var bench = new Benchmark(
"foo test", // 測試的名稱
foo, // 要測試的函數(shù)(僅僅是內(nèi)容)
{
// .. // 額外的選項(參見文檔)
}
);
bench.hz; // 每秒鐘執(zhí)行的操作數(shù)
bench.stats.moe; // 誤差邊際
bench.stats.variance; // 所有樣本上的方差
// ..
比起我在這里的窺豹一斑,關(guān)于使用Benchmark.js還有 許多 需要學(xué)習(xí)的東西。不過重點是,為了給一段給定的JavaScript代碼建立一個公平,可靠,并且合法的性能基準(zhǔn)分析,Benchmark.js包攬了所有的復(fù)雜性。如果你想要試著對你的代碼進(jìn)行測試和基準(zhǔn)分析,這個庫應(yīng)當(dāng)是你第一個想到的地方。
我們在這里展示的是測試一個單獨操作X的用法,但是相當(dāng)常見的情況是你想要用X和Y進(jìn)行比較。這可以通過簡單地在一個“Suite”(一個Benchmark.js的組織特性)中建立兩個測試來很容易做到。然后,你對照地運行它們,然后比較統(tǒng)計結(jié)果來對為什么X或Y更快做出論斷。
Benchmark.js理所當(dāng)然地可以被用于在瀏覽器中測試JavaScript(參見本章稍后的“jsPerf.com”一節(jié)),但它也可以運行在非瀏覽器環(huán)境中(Node.js等等)。
一個很大程度上沒有觸及的Benchmark.js的潛在用例是,在你的Dev或QA環(huán)境中針對你的應(yīng)用程序的JavaScript的關(guān)鍵路徑運行自動化的性能回歸測試。與在部署之前你可能運行單元測試的方式相似,你也可以將性能與前一次基準(zhǔn)分析進(jìn)行比較,來觀測你是否改進(jìn)或惡化了應(yīng)用程序性能。
Setup/Teardown
在前一個代碼段中,我們略過了“額外選項(extra options)”{ .. }
對象。但是這里有兩個我們應(yīng)當(dāng)討論的選項setup
和teardown
。
這兩個選項讓你定義在你的測試用例開始運行前和運行后被調(diào)用的函數(shù)。
一個需要理解的極其重要的事情是,你的setup
和teardown
代碼 不會為每一次測試迭代而運行。考慮它的最佳方式是,存在一個外部循環(huán)(重復(fù)的輪回),和一個內(nèi)部循環(huán)(重復(fù)的測試迭代)。setup
和teardown
會在每個 外部 循環(huán)(也就是輪回)迭代的開始和末尾運行,但不是在內(nèi)部循環(huán)。
為什么這很重要?讓我們想象你有一個看起來像這樣的測試用例:
a = a + "w";
b = a.charAt( 1 );
然后,你這樣建立你的測試setup
:
var a = "x";
你的意圖可能是相信對每一次測試迭代a
都以值"x"
開始。
但它不是!它使a
在每一次測試輪回中以"x"
開始,而后你的反復(fù)的+ "w"
連接將使a
的值越來越大,即便你永遠(yuǎn)唯一訪問的是位于位置1
的字符"w"
。
當(dāng)你想利用副作用來改變某些東西比如DOM,向它追加一個子元素時,這種意外經(jīng)常會咬到你。你可能認(rèn)為的父元素每次都被設(shè)置為空,但他實際上被追加了許多元素,而這可能會顯著地歪曲你的測試結(jié)果。
上下文為王
不要忘了檢查一個指定的性能基準(zhǔn)分析的上下文環(huán)境,特別是在X與Y之間進(jìn)行比較時。僅僅因為你的測試顯示X比Y速度快,并不意味著“X比Y快”這個結(jié)論是實際上有意義的。
舉個例子,讓我們假定一個性能測試顯示出X每秒可以運行1千萬次操作,而Y每秒運行8百萬次。你可以聲稱Y比X慢20%,而且在數(shù)學(xué)上你是對的,但是你的斷言并不向像你認(rèn)為的那么有用。
讓我們更加苛刻地考慮這個測試結(jié)果:每秒1千萬次操作就是每毫秒1萬次操作,就是每微秒10次操作。換句話說,一次操作要花0.1毫秒,或者100納秒。很難體會100納秒到底有多小,可以這樣比較一下,通常認(rèn)為人類的眼睛一般不能分辨小于100毫秒的變化,而這要比X操作的100納秒的速度慢100萬倍。
即便最近的科學(xué)研究顯示,大腦可能的最快處理速度是13毫秒(比先前的論斷快大約8倍),這意味著X的運行速度依然要比人類大腦可以感知事情的發(fā)生要快12萬5千倍。X運行的非常,非常快。
但更重要的是,讓我們來談?wù)刋與Y之間的不同,每秒2百萬次的差。如果X花100納秒,而Y花80納秒,差就是20納秒,也就是人類大腦可以感知的間隔的65萬分之一。
我要說什么?這種性能上的差別根本就一點兒都不重要!
但是等一下,如果這種操作將要一個接一個地發(fā)生許多次呢?那么差異就會累加起來,對吧?
好的,那么我們就要問,操作X有多大可能性將要一次又一次,一個接一個地運行,而且為了人類大腦能夠感知的一線希望而不得不發(fā)生65萬次。而且,它不得不在一個緊湊的循環(huán)中發(fā)生5百萬到1千萬次,才能接近于有意義。
雖然你們之中的計算機科學(xué)家會反對說這是可能的,但是你們之中的現(xiàn)實主義者們應(yīng)當(dāng)對這究竟有多大可能性進(jìn)行可行性檢查。即使在極其稀少的偶然中這有實際意義,但是在絕大多數(shù)情況下它沒有。
你們大量的針對微小操作的基準(zhǔn)分析結(jié)果——比如++x
對x++
的神話——完全是偽命題,只不過是用來支持在性能的基準(zhǔn)上X應(yīng)當(dāng)取代Y的結(jié)論。
引擎優(yōu)化
你根本無法可靠地這樣推斷:如果在你的獨立測試中X要比Y快10微秒,這意味著X總是比Y快所以應(yīng)當(dāng)總是被使用。這不是性能的工作方式。它要復(fù)雜太多了。
舉個例子,讓我們想象(純粹地假想)你在測試某些行為的微觀性能,比如比較:
var twelve = "12";
var foo = "foo";
// 測試 1
var X1 = parseInt( twelve );
var X2 = parseInt( foo );
// 測試 2
var Y1 = Number( twelve );
var Y2 = Number( foo );
如果你明白與Number(..)
比起來parseInt(..)
做了什么,你可能會在直覺上認(rèn)為parseInt(..)
潛在地有“更多工作”要做,特別是在foo
的測試用例下。或者你可能在直覺上認(rèn)為在foo
的測試用例下它們應(yīng)當(dāng)有同樣多的工作要做,因為它們倆應(yīng)當(dāng)能夠在第一個字符"f"
處停下。
哪一種直覺正確?老實說我不知道。但是我會制造一個與你的直覺無關(guān)的測試用例。當(dāng)你測試它的時候結(jié)果會是什么?我又一次在這里制造一個純粹的假想,我們沒實際上嘗試過,我也不關(guān)心。
讓我們假裝X
與Y
的測試結(jié)果在統(tǒng)計上是相同的。那么你關(guān)于"f"
字符上發(fā)生的事情的直覺得到確認(rèn)了嗎?沒有。
在我們的假想中可能發(fā)生這樣的事情:引擎可能會識別出變量twelve
和foo
在每個測試中僅被使用了一次,因此它可能會決定要內(nèi)聯(lián)這些值。然后它可能發(fā)現(xiàn)Number("12")
可以替換為12
。而且也許在parseInt(..)
上得到相同的結(jié)論,也許不會。
或者一個引擎的死代碼移除啟發(fā)式算法會攪和進(jìn)來,而且它發(fā)現(xiàn)變量X
和Y
都沒有被使用,所以聲明它們是沒有意義的,所以最終在任一個測試中都不做任何事情。
而且所有這些都只是關(guān)于一個單獨測試運行的假設(shè)而言的。比我們在這里用直覺想象的,現(xiàn)代的引擎復(fù)雜得更加難以置信。它們會使用所有的招數(shù),比如追蹤并記錄一段代碼在一段很短的時間內(nèi)的行為,或者使用一組特別限定的輸入。
如果引擎由于固定的輸入而用特定的方法進(jìn)行了優(yōu)化,但是在你的真實的程序中你給出了更多種類的輸入,以至于優(yōu)化機制決定使用不同的方式呢(或者根本不優(yōu)化!)?或者如果因為引擎看到代碼被基準(zhǔn)分析工具運行了成千上萬次而進(jìn)行了優(yōu)化,但在你的真實程序中它將僅會運行大約100次,而在這些條件下引擎認(rèn)定優(yōu)化不值得呢?
所有這些我們剛剛假想的優(yōu)化措施可能會發(fā)生在我們的被限定的測試中,但在更復(fù)雜的程序中引擎可能不會那么做(由于種種原因)。或者正相反——引擎可能不會優(yōu)化這樣不起眼的代碼,但是可能會更傾向于在系統(tǒng)已經(jīng)被一個更精巧的程序消耗后更加積極地優(yōu)化。
我想要說的是,你不能確切地知道這背后究竟發(fā)生了什么。你能搜羅的所有猜測和假想幾乎不會提煉成任何堅實的依據(jù)。
難道這意味著你不能真正地做有用的測試了嗎?絕對不是!
這可以歸結(jié)為測試 不真實 的代碼會給你 不真實 的結(jié)果。在盡可能的情況下,你應(yīng)當(dāng)測試真實的,有意義的代碼段,并且在最接近你實際能夠期望的真實條件下進(jìn)行。只有這樣你得到的結(jié)果才有機會模擬現(xiàn)實。
像++x
和x++
這樣的微觀基準(zhǔn)分析簡直和偽命題一模一樣,我們也許應(yīng)該直接認(rèn)為它就是。
jsPerf.com
雖然Bechmark.js對于在你使用的任何JS環(huán)境中測試代碼性能很有用,但是如果你需要從許多不同的環(huán)境(桌面瀏覽器,移動設(shè)備等)匯總測試結(jié)果并期望得到可靠的測試結(jié)論,它就顯得能力不足。
舉例來說,Chrome在高端的桌面電腦上與Chrome移動版在智能手機上的表現(xiàn)就大相徑庭。而一個充滿電的智能手機與一個只剩2%電量,設(shè)備開始降低無線電和處理器的能源供應(yīng)的智能手機的表現(xiàn)也完全不同。
如果在橫跨多于一種環(huán)境的情況下,你想在任何合理的意義上宣稱“X比Y快”,那么你就需要實際測試盡可能多的真實世界的環(huán)境。只因為Chrome執(zhí)行某種X操作比Y快并不意味著所有的瀏覽器都是這樣。而且你還可能想要根據(jù)你的用戶的人口統(tǒng)計交叉參照多種瀏覽器測試運行的結(jié)果。
有一個為此目的而生的牛X網(wǎng)站,稱為jsPerf(http://jsperf.com)。它使用我們前面提到的Benchmark.js庫來運行統(tǒng)計上正確且可靠的測試,并且可以讓測試運行在一個你可交給其他人的公開URL上。
每當(dāng)一個測試運行后,其結(jié)果都被收集并與這個測試一起保存,同時累積的測試結(jié)果將在網(wǎng)頁上被繪制成圖供所有人閱覽。
當(dāng)在這個網(wǎng)站上創(chuàng)建測試時,你一開始有兩個測試用例可以填寫,但你可以根據(jù)需要添加任意多個。你還可以建立在每次測試輪回開始時運行的setup
代碼,和在每次測試輪回結(jié)束前運行的teardown
代碼。
注意: 一個只做一個測試用例(如果你只對一個方案進(jìn)行基準(zhǔn)分析而不是相互對照)的技巧是,在第一次創(chuàng)建時使用輸入框的占位提示文本填寫第二個測試輸入框,之后編輯這個測試并將第二個測試留為空白,這樣它就會被刪除。你可以稍后添加更多測試用例。
你可以頂一個頁面的初始配置(引入庫文件,定義工具函數(shù),聲明變量,等等)。如有需要這里也有選項可以定義setup和teardow行為——參照前面關(guān)于Benchmark.js的討論中的“Setup/Teardown”一節(jié)。
可行性檢查
jsPerf是一個奇妙的資源,但它上面有許多公開的糟糕測試,當(dāng)你分析它們時會發(fā)現(xiàn),由于在本章目前為止羅列的各種原因,它們有很大的漏洞或者是偽命題。
考慮:
// 用例 1
var x = [];
for (var i=0; i<10; i++) {
x[i] = "x";
}
// 用例 2
var x = [];
for (var i=0; i<10; i++) {
x[x.length] = "x";
}
// 用例 3
var x = [];
for (var i=0; i<10; i++) {
x.push( "x" );
}
關(guān)于這個測試場景有一些現(xiàn)象值得我們深思:
開發(fā)者們在測試用例中加入自己的循環(huán)極其常見,而他們忘記了Benchmark.js已經(jīng)做了你所需要的所有反復(fù)。這些測試用例中的
for
循環(huán)有很大的可能是完全不必要的噪音。-
在每一個測試用例中都包含了
x
的聲明與初始化,似乎是不必要的。回想早前如果x = []
存在于setup
代碼中,它實際上不會在每一次測試迭代前執(zhí)行,而是在每一個輪回的開始執(zhí)行一次。這意味這x
將會持續(xù)地增長到非常大,而不僅是for
循環(huán)中暗示的大小10
。那么這是有意確保測試僅被限制在很小的數(shù)組上(大小為
10
)來觀察JS引擎如何動作?這 可能 是有意的,但如果是,你就不得不考慮它是否過于關(guān)注內(nèi)微妙的部實現(xiàn)細(xì)節(jié)了。另一方面,這個測試的意圖包含數(shù)組實際上會增長到非常大的情況嗎?JS引擎對大數(shù)組的行為與真實世界中預(yù)期的用法相比有意義且正確嗎?
它的意圖是要找出
x.length
或x.push(..)
在數(shù)組x
的追加操作上拖慢了多少性能嗎?好吧,這可能是一個合法的測試。但再一次,push(..)
是一個函數(shù)調(diào)用,所以它理所當(dāng)然地要比[..]
訪問慢。可以說,用例1與用例2比用例3更合理。
這里有另一個展示蘋果比橘子的常見漏洞的例子:
// 用例 1
var x = ["John","Albert","Sue","Frank","Bob"];
x.sort();
// 用例 2
var x = ["John","Albert","Sue","Frank","Bob"];
x.sort( function mySort(a,b){
if (a < b) return -1;
if (a > b) return 1;
return 0;
} );
這里,明顯的意圖是要找出自定義的mySort(..)
比較器比內(nèi)建的默認(rèn)比較器慢多少。但是通過將函數(shù)mySort(..)
作為內(nèi)聯(lián)的函數(shù)表達(dá)式生命,你就創(chuàng)建了一個不合理的/偽命題的測試。這里,第二個測試用例不僅測試用戶自定義的JS函數(shù),而且它還測試為每一個迭代創(chuàng)建一個新的函數(shù)表達(dá)式。
不知這會不會嚇到你,如果你運行一個相似的測試,但是將它更改為比較內(nèi)聯(lián)函數(shù)表達(dá)式與預(yù)先聲明的函數(shù),內(nèi)聯(lián)函數(shù)表達(dá)式的創(chuàng)建可能要慢2%到20%!
除非你的測試的意圖 就是 要考慮內(nèi)聯(lián)函數(shù)表達(dá)式創(chuàng)建的“成本”,一個更好/更合理的測試是將mySort(..)
的聲明放在頁面的setup中——不要放在測試的setup
中,因為這會為每次輪回進(jìn)行不必要的重復(fù)聲明——然后簡單地在測試用例中通過名稱引用它:x.sort(mySort)
。
基于前一個例子,另一種造成蘋果比橘子場景的陷阱是,不透明地對一個測試用例回避或添加“額外的工作”:
// 用例 1
var x = [12,-14,0,3,18,0,2.9];
x.sort();
// 用例 2
var x = [12,-14,0,3,18,0,2.9];
x.sort( function mySort(a,b){
return a - b;
} );
將先前提到的內(nèi)聯(lián)函數(shù)表達(dá)式陷阱放在一邊不談,第二個用例的mySort(..)
可以在這里工作是因為你給它提供了一組數(shù)字,而在字符串的情況下肯定會失敗。第一個用例不會扔出錯誤,但是它的實際行為將會不同而且會有不同的結(jié)果!這應(yīng)當(dāng)很明顯,但是:兩個測試用例之間結(jié)果的不同,幾乎可以否定了整個測試的合法性!
但是除了結(jié)果的不同,在這個用例中,內(nèi)建的sort(..)
比較器實際上要比mySort()
做了更多“額外的工作”,內(nèi)建的比較器將被比較的值轉(zhuǎn)換為字符串,然后進(jìn)行字典順序的比較。這樣第一個代碼段的結(jié)果為[-14, 0, 0, 12, 18, 2.9, 3]
而第二段代碼的結(jié)果為[-14, 0, 0, 2.9, 3, 12, 18]
(就測試的意圖來講可能更準(zhǔn)確)。
所以這個測試是不合理的,因為它的兩個測試用例實際上沒有做相同的任務(wù)。你得到的任何結(jié)果都將是偽命題。
這些同樣的陷阱可以微妙的多:
// 用例 1
var x = false;
var y = x ? 1 : 2;
// 用例 2
var x;
var y = x ? 1 : 2;
這里的意圖可能是要測試如果x
表達(dá)式不是Boolean的情況下,? :
操作符將要進(jìn)行的Boolean轉(zhuǎn)換對性能的影響(參見本系列的 類型與文法)。那么,根據(jù)在第二個用例中將會有額外的工作進(jìn)行轉(zhuǎn)換的事實,你看起來沒問題。
微妙的問題呢?你在第一個測試用例中設(shè)定了x
的值,而沒在另一個中設(shè)置,那么你實際上在第一個用例中做了在第二個用例中沒做的工作。為了消滅任何潛在的扭曲(盡管很微小),可以這樣:
// 用例 1
var x = false;
var y = x ? 1 : 2;
// 用例 2
var x = undefined;
var y = x ? 1 : 2;
現(xiàn)在兩個用例都有一個賦值了,這樣你想要測試的東西——x
的轉(zhuǎn)換或者不轉(zhuǎn)換——會更加正確的被隔離并測試。
編寫好的測試
來看看我能否清晰地表達(dá)我想在這里申明的更重要的事情。
好的測試作者需要細(xì)心地分析性地思考兩個測試用例之間存在什么樣的差別,和它們之間的差別是否是 有意的 或 無意的。
有意的差別當(dāng)然是正常的,但是產(chǎn)生歪曲結(jié)果的無意的差異實在太容易了。你不得不非常非常小心地回避這種歪曲。另外,你可能預(yù)期一個差異,但是你的意圖是什么對于你的測試的其他讀者來講不那么明顯,所以他們可能會錯誤地懷疑(或者相信!)你的測試。你如何搞定這個呢?
編寫更好,更清晰的測試。 另外,花些時間用文檔確切地記錄下你的測試意圖是什么(使用jsPerf.com的“Description”字段,或/和代碼注釋),即使是微小的細(xì)節(jié)。明確地表示有意的差別,這將幫助其他人和未來的你自己更好地找出那些可能歪曲測試結(jié)果的無意的差別。
將與你的測試無關(guān)的東西隔離開來,通過在頁面或測試的setup設(shè)置中預(yù)先聲明它們,使它們位于測試計時部分的外面。
與將你的真實代碼限制在很小的一塊,并脫離上下文環(huán)境來進(jìn)行基準(zhǔn)分析相比,測試與基準(zhǔn)分析在它們包含更大的上下文環(huán)境(但仍然有意義)時表現(xiàn)更好。這些測試將會趨向于運行得更慢,這意味著你發(fā)現(xiàn)的任何差別都在上下文環(huán)境中更有意義。
微觀性能
好了,直至現(xiàn)在我們一直圍繞著微觀性能的問題跳舞,并且一般上不贊成癡迷于它們。我想花一點兒時間直接解決它們。
當(dāng)你考慮對你的代碼進(jìn)行性能基準(zhǔn)分析時,第一件需要習(xí)慣的事情就是你寫的代碼不總是引擎實際運行的代碼。我們在第一章中討論編譯器的語句重排時簡單地看過這個話題,但是這里我們將要說明編譯器能有時決定運行與你編寫的不同的代碼,不僅是不同的順序,而是不同的替代品。
讓我們考慮這段代碼:
var foo = 41;
(function(){
(function(){
(function(baz){
var bar = foo + baz;
// ..
})(1);
})();
})();
你也許會認(rèn)為在最里面的函數(shù)的foo
引用需要做一個三層作用域查詢。我們在這個系列叢書的 作用域與閉包 一卷中涵蓋了詞法作用域如何工作,而事實上編譯器通常緩存這樣的查詢,以至于從不同的作用域引用foo
不會實質(zhì)上“花費”任何額外的東西。
但是這里有些更深刻的東西需要思考。如果編譯器認(rèn)識到foo
除了這一個位置外沒有被任何其他地方引用,進(jìn)而注意到它的值除了這里的41
外沒有任何變化會怎么樣呢?
JS編譯器能夠決定干脆完全移除foo
變量,并 內(nèi)聯(lián) 它的值是可能和可接受的,比如這樣:
(function(){
(function(){
(function(baz){
var bar = 41 + baz;
// ..
})(1);
})();
})();
注意: 當(dāng)然,編譯器可能也會對這里的baz
變量進(jìn)行相似的分析和重寫。
但你開始將你的JS代碼作為一種告訴引擎去做什么的提示或建議來考慮,而不是一種字面上的需求,你就會理解許多對零碎的語法細(xì)節(jié)的癡迷幾乎是毫無根據(jù)的。
另一個例子:
function factorial(n) {
if (n < 2) return 1;
return n * factorial( n - 1 );
}
factorial( 5 ); // 120
啊,一個老式的“階乘”算法!你可能會認(rèn)為JS引擎將會原封不動地運行這段代碼。老實說,它可能會——但我不是很確定。
但作為一段軼事,用C語言表達(dá)的同樣的代碼并使用先進(jìn)的優(yōu)化處理進(jìn)行編譯時,將會導(dǎo)致編譯器認(rèn)為factorial(5)
調(diào)用可以被替換為常數(shù)值120
,完全消除這個函數(shù)以及調(diào)用!
另外,一些引擎有一種稱為“遞歸展開(unrolling recursion)”的行為,它會意識到你表達(dá)的遞歸實際上可以用循環(huán)“更容易”(也就是更優(yōu)化地)地完成。前面的代碼可能會被JS引擎 重寫 為:
function factorial(n) {
if (n < 2) return 1;
var res = 1;
for (var i=n; i>1; i--) {
res *= i;
}
return res;
}
factorial( 5 ); // 120
現(xiàn)在,讓我們想象在前一個片段中你曾經(jīng)擔(dān)心n * factorial(n-1)
或n *= factorial(--n)
哪一個運行的更快。也許你甚至做了性能基準(zhǔn)分析來試著找出哪個更好。但是你忽略了一個事實,就是在更大的上下文環(huán)境中,引擎也許不會運行任何一行代碼,因為它可能展開了遞歸!
說到--
,--n
與n--
的對比,經(jīng)常被認(rèn)為可以通過選擇--n
的版本進(jìn)行優(yōu)化,因為理論上在匯編語言層面的處理上,它要做的努力少一些。
在現(xiàn)代的JavaScript中這種癡迷基本上是沒道理的。這種事情應(yīng)當(dāng)留給引擎來處理。你應(yīng)該編寫最合理的代碼。比較這三個for
循環(huán):
// 方式 1
for (var i=0; i<10; i++) {
console.log( i );
}
// 方式 2
for (var i=0; i<10; ++i) {
console.log( i );
}
// 方式 3
for (var i=-1; ++i<10; ) {
console.log( i );
}
就算你有一些理論支持第二或第三種選擇要比第一種的性能好那么一點點,充其量只能算是可疑,第三個循環(huán)更加使人困惑,因為為了使提前遞增的++i
被使用,你不得不讓i
從-1
開始來計算。而第一個與第二個選擇之間的區(qū)別實際上無關(guān)緊要。
這樣的事情是完全有可能的:JS引擎也許看到一個i++
被使用的地方,并意識到它可以安全地替換為等價的++i
,這意味著你決定挑選它們中的哪一個所花的時間完全被浪費了,而且這么做的產(chǎn)出毫無意義。
這是另外一個常見的愚蠢的癡迷于微觀性能的例子:
var x = [ .. ];
// 方式 1
for (var i=0; i < x.length; i++) {
// ..
}
// 方式 2
for (var i=0, len = x.length; i < len; i++) {
// ..
}
這里的理論是,你應(yīng)當(dāng)在變量len
中緩存數(shù)組x
的長度,因為從表面上看它不會改變,來避免在循環(huán)的每一次迭代中都查詢x.length
所花的開銷。
如果你圍繞x.length
的用法進(jìn)行性能基準(zhǔn)分析,與將它緩存在變量len
中的用法進(jìn)行比較,你會發(fā)現(xiàn)雖然理論聽起來不錯,但是在實踐中任何測量出的差異都是在統(tǒng)計學(xué)上完全沒有意義的。
事實上,在像v8這樣的引擎中,可以看到(http://mrale.ph/blog/2014/12/24/array-length-caching.html)通過提前緩存長度而不是讓引擎幫你處理它會使事情稍稍惡化。不要嘗試在聰明上戰(zhàn)勝你的JavaScript引擎,當(dāng)它來到性能優(yōu)化的地方時你可能會輸給它。
不是所有的引擎都一樣
在各種瀏覽器中的不同JS引擎可以稱為“規(guī)范兼容的”,雖然各自有完全不同的方式處理代碼。JS語言規(guī)范不要求與性能相關(guān)的任何事情——除了將在本章稍后將要講解的ES6“尾部調(diào)用優(yōu)化(Tail Call Optimization)”。
引擎可以自由決定哪一個操作將會受到它的關(guān)注而被優(yōu)化,也許代價是在另一種操作上的性能降低一些。要為一種操作找到一種在所有的瀏覽器中總是運行的更快的方式是非常不現(xiàn)實的。
在JS開發(fā)者社區(qū)的一些人發(fā)起了一項運動,特別是那些使用Node.js工作的人,去分析v8 JavaScript引擎的具體內(nèi)部實現(xiàn)細(xì)節(jié),并決定如何編寫定制的JS代碼來最大限度的利用v8的工作方式。通過這樣的努力你實際上可以在性能優(yōu)化上達(dá)到驚人的高度,所以這種努力的收益可能十分高。
一些針對v8的經(jīng)常被引用的例子是(https://github.com/petkaantonov/bluebird/wiki/Optimization-killers) :
- 不要將
arguments
變量從一個函數(shù)傳遞到任何其他函數(shù)中,因為這樣的“泄露”放慢了函數(shù)實現(xiàn)。 - 將一個
try..catch
隔離到它自己的函數(shù)中。瀏覽器在優(yōu)化任何含有try..catch
的函數(shù)時都會苦苦掙扎,所以將這樣的結(jié)構(gòu)移動到它自己的函數(shù)中意味著你持有不可優(yōu)化的危害的同時,讓其周圍的代碼是可以優(yōu)化的。
但與其聚焦在這些具體的竅門上,不如讓我們在一般意義上對v8專用的優(yōu)化方式進(jìn)行一下合理性檢驗。
你真的在編寫僅僅需要在一種JS引擎上運行的代碼嗎?即便你的代碼 當(dāng)前 是完全為了Node.js,那么假設(shè)v8將 總是 被使用的JS引擎可靠嗎?從現(xiàn)在開始的幾年以后的某一天,你有沒有可能會選擇除了Node.js之外的另一種服務(wù)器端JS平臺來運行你的程序?如果你以前所做的優(yōu)化現(xiàn)在在新的引擎上成為了執(zhí)行這種操作的很慢的方式怎么辦?
或者如果你的代碼總是在v8上運行,但是v8在某個時點決定改變一組操作的工作方式,是的曾經(jīng)快的現(xiàn)在變慢了,曾經(jīng)慢的變快了呢?
這些場景也都不只是理論上的。曾經(jīng),將多個字符串值放在一個數(shù)組中然后在這個數(shù)組上調(diào)用join("")
來連接這些值,要比僅使用+
直接連接這些值要快。這件事的歷史原因很微妙,但它與字符串值如何被存儲和在內(nèi)存中如何管理的內(nèi)部實現(xiàn)細(xì)節(jié)有關(guān)。
結(jié)果,當(dāng)時在業(yè)界廣泛傳播的“最佳實踐”建議開發(fā)者們總是使用數(shù)組join(..)
的方式。而且有許多人遵循了。
但是,某一天,JS引擎改變了內(nèi)部管理字符串的方式,而且特別在+
連接上做了優(yōu)化。他們并沒有放慢join(..)
,但是他們在幫助+
用法上做了更多的努力,因為它依然十分普遍。
注意: 某些特定方法的標(biāo)準(zhǔn)化和優(yōu)化的實施,很大程度上決定于它被使用的廣泛程度。這經(jīng)常(隱喻地)稱為“paving the cowpath”(不提前做好方案,而是等到事情發(fā)生了再去應(yīng)對)。
一旦處理字符串和連接的新方式定型,所有在世界上運行的,使用數(shù)組join(..)
來連接字符串的代碼都不幸地變成了次優(yōu)的方式。
另一個例子:曾經(jīng),Opera瀏覽器在如何處理基本包裝對象的封箱/拆箱(參見本系列的 類型與文法)上與其他瀏覽器不同。因此他們給開發(fā)者的建議是,如果一個原生string
值的屬性(如length
)或方法(如charAt(..)
)需要被訪問,就使用一個String
對象取代它。這個建議也許對那時的Opera是正確的,但是對于同時代的其他瀏覽器來說簡直就是完全相反的,因為它們都對原生string
進(jìn)行了專門的優(yōu)化,而不是對它們的包裝對象。
我認(rèn)為即使是對今天的代碼,這種種陷阱即便可能性不高,至少也是可能的。所以對于在我的JS代碼中單純地根據(jù)引擎的實現(xiàn)細(xì)節(jié)來進(jìn)行大范圍的優(yōu)化這件事來說我會非常小心,特別是如果這些細(xì)節(jié)僅對一種引擎成立時。
反過來也有一些事情需要警惕:你不應(yīng)當(dāng)為了繞過某一種引擎難于處理的地方而改變一塊代碼。
歷史上,IE是導(dǎo)致許多這種挫折的領(lǐng)頭羊,在老版本的IE中曾經(jīng)有許多場景,在當(dāng)時的其他主流瀏覽器中看起來沒有太多麻煩的性能方面苦苦掙扎。我們剛剛討論的字符串連接在IE6和IE7的年代就是一個真實的問題,那時候使用join(..)
就可能要比使用+
能得到更好的性能。
不過為了一種瀏覽器的性能問題而使用一種很有可能在其他所有瀏覽器上是次優(yōu)的編碼方式,很難說是正當(dāng)?shù)摹<幢氵@種瀏覽器占有了你的網(wǎng)站用戶的很大市場份額,編寫恰當(dāng)?shù)拇a并仰仗瀏覽器最終在更好的優(yōu)化機制上更新自己可能更實際。
“沒什么是比暫時的黑科技更永恒的。”你現(xiàn)在為了繞過一些性能的Bug而編寫的代碼可能要比這個Bug在瀏覽器中存在的時間長的多。
在那個瀏覽器每五年才更新一次的年代,這是個很難做的決定。但是如今,所有的瀏覽器都在快速地更新(雖然移動端的世界還有些滯后),而且它們都在競爭而使得web優(yōu)化特性變得越來越好。
如果你真的碰到了一個瀏覽器有其他瀏覽器沒有的性能瑕疵,那么就確保用你一切可用的手段來報告它。絕大多數(shù)瀏覽器都有為此而公開的Bug追跡系統(tǒng)。
提示: 我只建議,如果一個在某種瀏覽器中的性能問題真的是極端攪局的問題時才繞過它,而不是僅僅因為它使人厭煩或沮喪。而且我會非常小心地檢查這種性能黑科技有沒有在其他瀏覽器中產(chǎn)生負(fù)面影響。
大局
與擔(dān)心所有這些微觀性能的細(xì)節(jié)相反,我們應(yīng)但關(guān)注大局類型的優(yōu)化。
你怎么知道什么東西是不是大局的?你首先必須理解你的代碼是否運行在關(guān)鍵路徑上。如果它沒在關(guān)鍵路徑上,你的優(yōu)化可能就沒有太大價值。
“這是過早的優(yōu)化!”你聽過這種訓(xùn)誡嗎?它源自Donald Knuth的一段著名的話:“過早的優(yōu)化是萬惡之源。”。許多開發(fā)者都引用這段話來說明大多數(shù)優(yōu)化都是“過早”的而且是一種精力的浪費。事實是,像往常一樣,更加微妙。
這是Knuth在語境中的原話:
程序員們浪費了大量的時間考慮,或者擔(dān)心,他們的程序中的 不關(guān)鍵 部分的速度,而在考慮調(diào)試和維護(hù)時這些在效率上的企圖實際上有很強大的負(fù)面影響。我們應(yīng)當(dāng)忘記微小的效率,可以說在大概97%的情況下:過早的優(yōu)化是萬惡之源。然而我們不應(yīng)該忽略那 關(guān)鍵的 3%中的機會。[強調(diào)]
(http://web.archive.org/web/20130731202547/http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf, Computing Surveys, Vol 6, No 4, December 1974)
我相信這樣轉(zhuǎn)述Knuth的 意思 是合理的:“非關(guān)鍵路徑的優(yōu)化是萬惡之源。”所以問題的關(guān)鍵是弄清楚你的代碼是否在關(guān)鍵路徑上——你因該優(yōu)化它!——或者不。
我甚至可以激進(jìn)地這么說:沒有花在優(yōu)化關(guān)鍵路徑上的時間是浪費的,不管它的效果多么微小。沒有花在優(yōu)化非關(guān)鍵路徑上的時間是合理的,不管它的效果多么大。
如果你的代碼在關(guān)鍵路徑上,比如將要一次又一次被運行的“熱”代碼塊兒,或者在用戶將要注意到的UX關(guān)鍵位置,比如循環(huán)動畫或者CSS樣式更新,那么你應(yīng)當(dāng)不遺余力地進(jìn)行有意義的,可測量的重大優(yōu)化。
舉個例子,考慮一個動畫循環(huán)的關(guān)鍵路徑,它需要將一個字符串值轉(zhuǎn)換為一個數(shù)字。這當(dāng)然有多種方法做到,但是哪一個是最快的呢?
var x = "42"; // 需要數(shù)字 `42`
// 選擇1:讓隱式強制轉(zhuǎn)換自動完成工作
var y = x / 2;
// 選擇2:使用`parseInt(..)`
var y = parseInt( x, 0 ) / 2;
// 選擇3:使用`Number(..)`
var y = Number( x ) / 2;
// 選擇4:使用`+`二元操作符
var y = +x / 2;
// 選擇5:使用`|`二元操作符
var y = (x | 0) / 2;
注意: 我將這個問題留作給讀者們的練習(xí),如果你對這些選擇之間性能上的微小區(qū)別感興趣的話,可以做一個測試。
當(dāng)你考慮這些不同的選擇時,就像人們說的,“有一個和其他的不一樣。”parseInt(..)
可以工作,但它做的事情多的多——它會解析字符串而不是轉(zhuǎn)換它。你可能會正確地猜想parseInt(..)
是一個更慢的選擇,而你可能應(yīng)當(dāng)避免使用它。
當(dāng)然,如果x
可能是一個 需要被解析 的值,比如"42px"
(比如CSS樣式查詢),那么parseInt(..)
確實是唯一合適的選擇!
Number(..)
也是一個函數(shù)調(diào)用。從行為的角度講,它與+
二元操作符是相同的,但它事實上可能慢一點兒,需要更多的機器指令運轉(zhuǎn)來執(zhí)行這個函數(shù)。當(dāng)然,JS引擎也可能識別出了這種行為上的對稱性,而僅僅為你處理Number(..)
行為的內(nèi)聯(lián)形式(也就是+x
)!
但是要記住,癡迷于+x
和x | 0
的比較在大多數(shù)情況下都是浪費精力。這是一個微觀性能問題,而且你不應(yīng)該讓它使你的程序的可讀性降低。
雖然你的程序的關(guān)鍵路徑性能非常重要,但它不是唯一的因素。在幾種性能上大體相似的選擇中,可讀性應(yīng)當(dāng)是另一個重要的考量。
尾部調(diào)用優(yōu)化 (TCO)
正如我們早前簡單提到的,ES6包含了一個冒險進(jìn)入性能世界的具體需求。它是關(guān)于在函數(shù)調(diào)用時可能會發(fā)生的一種具體的優(yōu)化形式:尾部調(diào)用優(yōu)化(TCO)。
簡單地說,一個“尾部調(diào)用”是一個出現(xiàn)在另一個函數(shù)“尾部”的函數(shù)調(diào)用,于是在這個調(diào)用完成后,就沒有其他的事情要做了(除了也許要返回結(jié)果值)。
例如,這是一個帶有尾部調(diào)用的非遞歸形式:
function foo(x) {
return x;
}
function bar(y) {
return foo( y + 1 ); // 尾部調(diào)用
}
function baz() {
return 1 + bar( 40 ); // 不是尾部調(diào)用
}
baz(); // 42
foo(y+1)
是一個在bar(..)
中的尾部調(diào)用,因為在foo(..)
完成之后,bar(..)
也即而完成,除了在這里需要返回foo(..)
調(diào)用的結(jié)果。然而,bar(40)
不是 一個尾部調(diào)用,因為在它完成后,在baz()
能返回它的結(jié)果前,這個結(jié)果必須被加1。
不過于深入本質(zhì)細(xì)節(jié)而簡單地說,調(diào)用一個新函數(shù)需要保留額外的內(nèi)存來管理調(diào)用棧,它稱為一個“棧幀(stack frame)”。所以前面的代碼段通常需要同時為baz()
,bar(..)
,和foo(..)
都準(zhǔn)備一個棧幀。
然而,如果一個支持TCO的引擎可以認(rèn)識到foo(y+1)
調(diào)用位于 尾部位置 意味著bar(..)
基本上完成了,那么當(dāng)調(diào)用foo(..)
時,它就并沒有必要創(chuàng)建一個新的棧幀,而是可以重復(fù)利用既存的bar(..)
的棧幀。這不僅更快,而且也更節(jié)省內(nèi)存。
在一個簡單的代碼段中,這種優(yōu)化機制沒什么大不了的,但是當(dāng)對付遞歸,特別是當(dāng)遞歸會造成成百上千的棧幀時,它就變成了 相當(dāng)有用的技術(shù)。引擎可以使用TCO在一個棧幀內(nèi)完成所有調(diào)用!
在JS中遞歸是一個令人不安的話題,因為沒有TCO,引擎就不得不實現(xiàn)一個隨意的(而且各不相同的)限制,規(guī)定它們允許遞歸棧能有多深,來防止內(nèi)存耗盡。使用TCO,帶有 尾部位置 調(diào)用的遞歸函數(shù)實質(zhì)上可以沒有邊界地運行,因為從沒有額外的內(nèi)存使用!
考慮前面的遞歸factorial(..)
,但是將它重寫為對TCO友好的:
function factorial(n) {
function fact(n,res) {
if (n < 2) return res;
return fact( n - 1, n * res );
}
return fact( n, 1 );
}
factorial( 5 ); // 120
這個版本的factorial(..)
仍然是遞歸的,而且它還是可以進(jìn)行TCO優(yōu)化的,因為兩個內(nèi)部的fact(..)
調(diào)用都在 尾部位置。
注意: 一個需要注意的重點是,TCO盡在尾部調(diào)用實際存在時才會實施。如果你沒用尾部調(diào)用編寫遞歸函數(shù),性能機制將仍然退回到普通的棧幀分配,而且引擎對于這樣的遞歸的調(diào)用棧限制依然有效。許多遞歸函數(shù)可以像我們剛剛展示的factorial(..)
那樣重寫,但是要小心處理細(xì)節(jié)。
ES6要求各個引擎實現(xiàn)TCO而不是留給它們自行考慮的原因之一是,由于對調(diào)用棧限制的恐懼,缺少TCO 實際上趨向于減少特定的算法在JS中使用遞歸實現(xiàn)的機會。
如果無論什么情況下引擎缺少TCO只是安靜地退化到性能差一些的方式上,那么它可能不會是ES6需要 要求 的東西。但是因為缺乏TCO可能會實際上使特定的程序不現(xiàn)實,所以與其說它只是一種隱藏的實現(xiàn)細(xì)節(jié),不如說它是一個重要的語言特性更合適。
ES6保證,從現(xiàn)在開始,JS開發(fā)者們能夠在所有兼容ES6+的瀏覽器上信賴這種優(yōu)化機制。這是JS性能的一個勝利!
復(fù)習(xí)
有效地對一段代碼進(jìn)行性能基準(zhǔn)分析,特別是將它與同樣代碼的另一種寫法相比較來看哪一種方式更快,需要小心地關(guān)注細(xì)節(jié)。
與其運行你自己的統(tǒng)計學(xué)上合法的基準(zhǔn)分析邏輯,不如使用Benchmark.js庫,它會為你搞定。但要小心你如何編寫測試,因為太容易構(gòu)建一個看起來合法但實際上有漏洞的測試了——即使是一個微小的區(qū)別也會使結(jié)果歪曲到完全不可靠。
盡可能多地從不同的環(huán)境中得到盡可能多的測試結(jié)果來消除硬件/設(shè)備偏差很重要。jsPerf.com是一個用于大眾外包性能基準(zhǔn)分析測試的神奇網(wǎng)站。
許多常見的性能測試不幸地癡迷于無關(guān)緊要的微觀性能細(xì)節(jié),比如比較x++
和++x
。編寫好的測試意味著理解如何聚焦大局上關(guān)注的問題,比如在關(guān)鍵路徑上優(yōu)化,和避免落入不同JS引擎的實現(xiàn)細(xì)節(jié)的陷阱。
尾部調(diào)用優(yōu)化(TCO)是一個ES6要求的優(yōu)化機制,它會使一些以前在JS中不可能的遞歸模式變得可能。TCO允許一個位于另一個函數(shù)的 尾部位置 的函數(shù)調(diào)用不需要額外的資源就可以執(zhí)行,這意味著引擎不再需要對遞歸算法的調(diào)用棧深度設(shè)置一個隨意的限制了。