第六章: 基準分析與調(diào)優(yōu)

特別說明款慨,為便于查閱谭确,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS

本書的前四章都是關(guān)于代碼模式(異步與同步)的性能刊橘,而第五章是關(guān)于宏觀的程序結(jié)構(gòu)層面的性能讲岁,本章從微觀層面繼續(xù)性能的話題蔗崎,關(guān)注的焦點在一個表達式/語句上酵幕。

好奇心最重的一個領(lǐng)域——確實,一些開發(fā)者十分癡迷于此——是分析和測試如何寫一行或一塊兒代碼的各種選項缓苛,看哪一個更快芳撒。

我們將會看到這些問題中的一些,但重要的是要理解從最開始這一章就 不是 為了滿足對微性能調(diào)優(yōu)的癡迷,比如某種給定的JS引擎運行++a是否要比運行a++快笔刹。這一章更重要的目標是芥备,搞清楚哪種JS性能要緊而哪種不要緊,和如何指出這種不同舌菜。

但在我們達到目的之前萌壳,我們需要探索一下如何最準確和最可靠地測試JS性能,因為有太多的誤解和謎題充斥著我們集體主義崇拜的知識庫日月。我們需要將這些垃圾篩出去以便找到清晰的答案袱瓮。

基準分析(Benchmarking)

好了,是時候開始消除一些誤解了爱咬。我敢打賭尺借,廣大的JS開發(fā)者們,如果被問到如何測量一個特定操作的速度(執(zhí)行時間)台颠,將會一頭扎進這樣的東西:

var start = (new Date()).getTime(); // 或者`Date.now()`

// 做一些操作

var end = (new Date()).getTime();

console.log( "Duration:", (end - start) );

如果這大致就是你想到的褐望,請舉手。是的串前,我就知道你會這么想瘫里。這個方式有許多錯誤,但是別難過荡碾;我們都這么干過谨读。

這種測量到底告訴了你什么?對于當前的操作的執(zhí)行時間來說坛吁,理解它告訴了你什么和沒告訴你什么是學(xué)習(xí)如何正確測量JavaScript的性能的關(guān)鍵劳殖。

如果持續(xù)的時間報告為0,你也許會試圖認為它花的時間少于1毫秒拨脉。但是這不是非常準確哆姻。一些平臺不能精確到毫秒,反而是在更大的時間單位上更新計時器玫膀。舉個例子矛缨,老版本的windows(IE也是如此)只有15毫秒的精確度,這意味著要得到與0不同的報告帖旨,操作就必須至少要花這么長時間箕昭!

另外,不管被報告的持續(xù)時間是多少解阅,你唯一真實知道的是落竹,操作在當前這一次運行中大概花了這么長時間。你幾乎沒有信心說它將總是以這個速度運行货抄。你不知道引擎或系統(tǒng)是否在就在那個確切的時刻進行了干擾述召,而在其他的時候這個操作可能會運行的快一些朱转。

要是持續(xù)的時間報告為4呢?你確信它花了大概4毫秒桨武?不肋拔,它可能沒花那么長時間,而且在取得startend時間戳?xí)r會有一些其他的延遲呀酸。

更麻煩的是,你也不知道這個操作測試所在的環(huán)境是不是過于優(yōu)化了琼梆。這樣的情況是有可能的:JS引擎找到了一個辦法來優(yōu)化你的測試用例性誉,但是在更真實的程序中這樣的優(yōu)化將會被稀釋或者根本不可能,如此這個操作將會比你測試時運行的慢茎杂。

那么...我們知道什么错览?不幸的是,在這種狀態(tài)下煌往,我們幾乎什么都不知道倾哺。 可信度如此低的東西甚至不夠你建立自己的判斷。你的“基準分析”基本沒用刽脖。更糟的是羞海,它隱含的這種不成立的可信度很危險,不僅是對你曲管,而且對其他人也一樣:認為導(dǎo)致這些結(jié)果的條件不重要却邓。

重復(fù)

“好的,”你說院水,“在它周圍放一個循環(huán)腊徙,讓整個測試需要的時間長一些∶誓常”如果你重復(fù)一個操作100次撬腾,而整個循環(huán)在報告上說總共花了137ms,那么你可以除以100并得到每次操作平均持續(xù)時間1.37ms恢恼,對吧民傻?

其實,不確切厅瞎。

對于你打算在你的整個應(yīng)用程序范圍內(nèi)推廣的操作的性能饰潜,僅靠一個直白的數(shù)據(jù)上的平均做出判斷絕對是不夠的。在一百次迭代中和簸,即使是幾個極端值(或高或低)就可以歪曲平均值彭雾,而后當你反復(fù)實施這個結(jié)論時,你就更進一步擴大了這種歪曲锁保。

與僅僅運行固定次數(shù)的迭代不同薯酝,你可以選擇將測試的循環(huán)運行一個特定長的時間半沽。那可能更可靠,但是你如何決定運行多長時間吴菠?你可能會猜它應(yīng)該是你的操作運行一次所需時間的倍數(shù)者填。錯。

實際上做葵,循環(huán)持續(xù)的時間應(yīng)當基于你使用的計時器的精度占哟,具體地將不精確的 ·可能性最小化。你的計時器精度越低酿矢,你就需要運行更長時間來確保你將錯誤的概率最小化了榨乎。一個15ms的計時器對于精確的基準分析來說太差勁兒了;為了把它的不確定性(也就是“錯誤率”)最小化到低于1%瘫筐,你需要將測試的迭代循環(huán)運行750ms蜜暑。一個1ms的計時器只需要一個循環(huán)運行50ms就可以得到相同的可信度。

但策肝,這只是一個樣本肛捍。為了確信你排除了歪曲結(jié)果的因素,你將會想要許多樣本來求平均值之众。你還會想要明白最差的樣本有多慢拙毫,最佳的樣本有多快,最差與最佳的情況相差多少等等酝枢。你想知道的不僅是一個數(shù)字告訴你某個東西跑的多塊恬偷,而且還需要一個關(guān)于這個數(shù)字有多可信的量化表達。

另外帘睦,你可能想要組合這些不同的技術(shù)(還有其他的)袍患,以便于你可以在所有這些可能的方式中找到最佳的平衡。

這一切只不過是開始所需的最低限度的認識竣付。如果你曾經(jīng)使用比我剛才幾句話帶過的東西更不嚴謹?shù)姆绞竭M行基準分析诡延,那么...“你不懂:正確的基準分析”。

Benchmark.js

任何有用而且可靠的基準分析應(yīng)當基于統(tǒng)計學(xué)上的實踐古胆。我不是要在這里寫一章統(tǒng)計學(xué)肆良,所以我會帶過一些名詞:標準差,方差逸绎,誤差邊際惹恃。如果你不知道這些名詞意味著什么——我在大學(xué)上過統(tǒng)計學(xué)課程,而我依然對他們有點兒暈——那么實際上你沒有資格去寫你自己的基準分析邏輯棺牧。

幸運的是巫糙,一些像John-David Dalton和Mathias Bynens這樣的聰明家伙明白這些概念,并且寫了一個統(tǒng)計學(xué)上的基準分析工具颊乘,稱為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)講解細節(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代碼建立一個公平,可靠家妆,并且合法的性能基準分析,Benchmark.js包攬了所有的復(fù)雜性冕茅。如果你想要試著對你的代碼進行測試和基準分析伤极,這個庫應(yīng)當是你第一個想到的地方。

我們在這里展示的是測試一個單獨操作X的用法姨伤,但是相當常見的情況是你想要用X和Y進行比較哨坪。這可以通過簡單地在一個“Suite”(一個Benchmark.js的組織特性)中建立兩個測試來很容易做到。然后乍楚,你對照地運行它們当编,然后比較統(tǒng)計結(jié)果來對為什么X或Y更快做出論斷。

Benchmark.js理所當然地可以被用于在瀏覽器中測試JavaScript(參見本章稍后的“jsPerf.com”一節(jié))徒溪,但它也可以運行在非瀏覽器環(huán)境中(Node.js等等)忿偷。

一個很大程度上沒有觸及的Benchmark.js的潛在用例是,在你的Dev或QA環(huán)境中針對你的應(yīng)用程序的JavaScript的關(guān)鍵路徑運行自動化的性能回歸測試臊泌。與在部署之前你可能運行單元測試的方式相似鲤桥,你也可以將性能與前一次基準分析進行比較,來觀測你是否改進或惡化了應(yīng)用程序性能渠概。

Setup/Teardown

在前一個代碼段中茶凳,我們略過了“額外選項(extra options)”{ .. }對象。但是這里有兩個我們應(yīng)當討論的選項setupteardown播揪。

這兩個選項讓你定義在你的測試用例開始運行前和運行后被調(diào)用的函數(shù)贮喧。

一個需要理解的極其重要的事情是,你的setupteardown代碼 不會為每一次測試迭代而運行猪狈∠渎伲考慮它的最佳方式是,存在一個外部循環(huán)(重復(fù)的輪回)罪裹,和一個內(nèi)部循環(huán)(重復(fù)的測試迭代)饱普。setupteardown會在每個 外部 循環(huán)(也就是輪回)迭代的開始和末尾運行运挫,但不是在內(nèi)部循環(huán)。

為什么這很重要套耕?讓我們想象你有一個看起來像這樣的測試用例:

a = a + "w";
b = a.charAt( 1 );

然后谁帕,你這樣建立你的測試setup

var a = "x";

你的意圖可能是相信對每一次測試迭代a都以值"x"開始。

但它不是冯袍!它使a在每一次測試輪回中以"x"開始匈挖,而后你的反復(fù)的+ "w"連接將使a的值越來越大,即便你永遠唯一訪問的是位于位置1的字符"w"康愤。

當你想利用副作用來改變某些東西比如DOM儡循,向它追加一個子元素時,這種意外經(jīng)常會咬到你征冷。你可能認為的父元素每次都被設(shè)置為空择膝,但他實際上被追加了許多元素,而這可能會顯著地歪曲你的測試結(jié)果检激。

上下文為王

不要忘了檢查一個指定的性能基準分析的上下文環(huán)境肴捉,特別是在X與Y之間進行比較時。僅僅因為你的測試顯示X比Y速度快叔收,并不意味著“X比Y快”這個結(jié)論是實際上有意義的齿穗。

舉個例子,讓我們假定一個性能測試顯示出X每秒可以運行1千萬次操作饺律,而Y每秒運行8百萬次窃页。你可以聲稱Y比X慢20%,而且在數(shù)學(xué)上你是對的复濒,但是你的斷言并不向像你認為的那么有用脖卖。

讓我們更加苛刻地考慮這個測試結(jié)果:每秒1千萬次操作就是每毫秒1萬次操作,就是每微秒10次操作芝薇。換句話說胚嘲,一次操作要花0.1毫秒,或者100納秒洛二。很難體會100納秒到底有多小馋劈,可以這樣比較一下,通常認為人類的眼睛一般不能分辨小于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)當對這究竟有多大可能性進行可行性檢查斋陪。即使在極其稀少的偶然中這有實際意義翰撑,但是在絕大多數(shù)情況下它沒有。

你們大量的針對微小操作的基準分析結(jié)果——比如++xx++的神話——完全是偽命題匠题,只不過是用來支持在性能的基準上X應(yīng)當取代Y的結(jié)論拯坟。

引擎優(yōu)化

你根本無法可靠地這樣推斷:如果在你的獨立測試中X要比Y快10微秒,這意味著X總是比Y快所以應(yī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(..)做了什么,你可能會在直覺上認為parseInt(..)潛在地有“更多工作”要做盖淡,特別是在foo的測試用例下年柠。或者你可能在直覺上認為在foo的測試用例下它們應(yīng)當有同樣多的工作要做褪迟,因為它們倆應(yīng)當能夠在第一個字符"f"處停下冗恨。

哪一種直覺正確答憔?老實說我不知道。但是我會制造一個與你的直覺無關(guān)的測試用例掀抹。當你測試它的時候結(jié)果會是什么虐拓?我又一次在這里制造一個純粹的假想,我們沒實際上嘗試過渴丸,我也不關(guān)心侯嘀。

讓我們假裝XY的測試結(jié)果在統(tǒng)計上是相同的。那么你關(guān)于"f"字符上發(fā)生的事情的直覺得到確認了嗎谱轨?沒有戒幔。

在我們的假想中可能發(fā)生這樣的事情:引擎可能會識別出變量twelvefoo在每個測試中僅被使用了一次,因此它可能會決定要內(nèi)聯(lián)這些值土童。然后它可能發(fā)現(xiàn)Number("12")可以替換為12诗茎。而且也許在parseInt(..)上得到相同的結(jié)論,也許不會献汗。

或者一個引擎的死代碼移除啟發(fā)式算法會攪和進來敢订,而且它發(fā)現(xiàn)變量XY都沒有被使用,所以聲明它們是沒有意義的罢吃,所以最終在任一個測試中都不做任何事情楚午。

而且所有這些都只是關(guān)于一個單獨測試運行的假設(shè)而言的。比我們在這里用直覺想象的尿招,現(xiàn)代的引擎復(fù)雜得更加難以置信矾柜。它們會使用所有的招數(shù),比如追蹤并記錄一段代碼在一段很短的時間內(nèi)的行為就谜,或者使用一組特別限定的輸入怪蔑。

如果引擎由于固定的輸入而用特定的方法進行了優(yōu)化,但是在你的真實的程序中你給出了更多種類的輸入丧荐,以至于優(yōu)化機制決定使用不同的方式呢(或者根本不優(yōu)化@掳辍)?或者如果因為引擎看到代碼被基準分析工具運行了成千上萬次而進行了優(yōu)化虹统,但在你的真實程序中它將僅會運行大約100次弓坞,而在這些條件下引擎認定優(yōu)化不值得呢?

所有這些我們剛剛假想的優(yōu)化措施可能會發(fā)生在我們的被限定的測試中窟却,但在更復(fù)雜的程序中引擎可能不會那么做(由于種種原因)昼丑。或者正相反——引擎可能不會優(yōu)化這樣不起眼的代碼夸赫,但是可能會更傾向于在系統(tǒng)已經(jīng)被一個更精巧的程序消耗后更加積極地優(yōu)化菩帝。

我想要說的是,你不能確切地知道這背后究竟發(fā)生了什么。你能搜羅的所有猜測和假想幾乎不會提煉成任何堅實的依據(jù)呼奢。

難道這意味著你不能真正地做有用的測試了嗎宜雀?絕對不是!

這可以歸結(jié)為測試 不真實 的代碼會給你 不真實 的結(jié)果握础。在盡可能的情況下辐董,你應(yīng)當測試真實的,有意義的代碼段禀综,并且在最接近你實際能夠期望的真實條件下進行简烘。只有這樣你得到的結(jié)果才有機會模擬現(xiàn)實。

++xx++這樣的微觀基準分析簡直和偽命題一模一樣定枷,我們也許應(yīng)該直接認為它就是孤澎。

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上亚茬。

每當一個測試運行后,其結(jié)果都被收集并與這個測試一起保存浓恳,同時累積的測試結(jié)果將在網(wǎng)頁上被繪制成圖供所有人閱覽刹缝。

當在這個網(wǎng)站上創(chuàng)建測試時,你一開始有兩個測試用例可以填寫颈将,但你可以根據(jù)需要添加任意多個梢夯。你還可以建立在每次測試輪回開始時運行的setup代碼,和在每次測試輪回結(jié)束前運行的teardown代碼晴圾。

注意: 一個只做一個測試用例(如果你只對一個方案進行基準分析而不是相互對照)的技巧是颂砸,在第一次創(chuàng)建時使用輸入框的占位提示文本填寫第二個測試輸入框,之后編輯這個測試并將第二個測試留為空白,這樣它就會被刪除人乓。你可以稍后添加更多測試用例勤篮。

你可以頂一個頁面的初始配置(引入庫文件,定義工具函數(shù)色罚,聲明變量碰缔,等等)。如有需要這里也有選項可以定義setup和teardow行為——參照前面關(guān)于Benchmark.js的討論中的“Setup/Teardown”一節(jié)戳护。

可行性檢查

jsPerf是一個奇妙的資源金抡,但它上面有許多公開的糟糕測試,當你分析它們時會發(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)細節(jié)了。

    另一方面爬迟,這個測試的意圖包含數(shù)組實際上會增長到非常大的情況嗎橘蜜?JS引擎對大數(shù)組的行為與真實世界中預(yù)期的用法相比有意義且正確嗎?

  • 它的意圖是要找出x.lengthx.push(..)在數(shù)組x的追加操作上拖慢了多少性能嗎付呕?好吧计福,這可能是一個合法的測試。但再一次徽职,push(..)是一個函數(shù)調(diào)用象颖,所以它理所當然地要比[..]訪問慢∧范ぃ可以說说订,用例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)建的默認比較器慢多少克蚂。但是通過將函數(shù)mySort(..)作為內(nèi)聯(lián)的函數(shù)表達式生命闺鲸,你就創(chuàng)建了一個不合理的/偽命題的測試。這里埃叭,第二個測試用例不僅測試用戶自定義的JS函數(shù)摸恍,而且它還測試為每一個迭代創(chuàng)建一個新的函數(shù)表達式。

不知這會不會嚇到你赤屋,如果你運行一個相似的測試立镶,但是將它更改為比較內(nèi)聯(lián)函數(shù)表達式與預(yù)先聲明的函數(shù),內(nèi)聯(lián)函數(shù)表達式的創(chuàng)建可能要慢2%到20%类早!

除非你的測試的意圖 就是 要考慮內(nèi)聯(lián)函數(shù)表達式創(chuàng)建的“成本”媚媒,一個更好/更合理的測試是將mySort(..)的聲明放在頁面的setup中——不要放在測試的setup中,因為這會為每次輪回進行不必要的重復(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ù)表達式陷阱放在一邊不談逆日,第二個用例的mySort(..)可以在這里工作是因為你給它提供了一組數(shù)字嵌巷,而在字符串的情況下肯定會失敗。第一個用例不會扔出錯誤室抽,但是它的實際行為將會不同而且會有不同的結(jié)果搪哪!這應(yīng)當很明顯,但是:兩個測試用例之間結(jié)果的不同坪圾,幾乎可以否定了整個測試的合法性晓折!

但是除了結(jié)果的不同,在這個用例中兽泄,內(nèi)建的sort(..)比較器實際上要比mySort()做了更多“額外的工作”漓概,內(nèi)建的比較器將被比較的值轉(zhuǎn)換為字符串,然后進行字典順序的比較病梢。這樣第一個代碼段的結(jié)果為[-14, 0, 0, 12, 18, 2.9, 3]而第二段代碼的結(jié)果為[-14, 0, 0, 2.9, 3, 12, 18](就測試的意圖來講可能更準確)垛耳。

所以這個測試是不合理的,因為它的兩個測試用例實際上沒有做相同的任務(wù)飘千。你得到的任何結(jié)果都將是偽命題。

這些同樣的陷阱可以微妙的多:

// 用例 1
var x = false;
var y = x ? 1 : 2;

// 用例 2
var x;
var y = x ? 1 : 2;

這里的意圖可能是要測試如果x表達式不是Boolean的情況下栈雳,? :操作符將要進行的Boolean轉(zhuǎn)換對性能的影響(參見本系列的 類型與文法)护奈。那么,根據(jù)在第二個用例中將會有額外的工作進行轉(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)換——會更加正確的被隔離并測試。

編寫好的測試

來看看我能否清晰地表達我想在這里申明的更重要的事情蚌讼。

好的測試作者需要細心地分析性地思考兩個測試用例之間存在什么樣的差別辟灰,和它們之間的差別是否是 有意的無意的

有意的差別當然是正常的篡石,但是產(chǎn)生歪曲結(jié)果的無意的差異實在太容易了芥喇。你不得不非常非常小心地回避這種歪曲。另外凰萨,你可能預(yù)期一個差異继控,但是你的意圖是什么對于你的測試的其他讀者來講不那么明顯,所以他們可能會錯誤地懷疑(或者相信E志臁)你的測試武通。你如何搞定這個呢?

編寫更好瘦材,更清晰的測試厅须。 另外,花些時間用文檔確切地記錄下你的測試意圖是什么(使用jsPerf.com的“Description”字段食棕,或/和代碼注釋)朗和,即使是微小的細節(jié)。明確地表示有意的差別簿晓,這將幫助其他人和未來的你自己更好地找出那些可能歪曲測試結(jié)果的無意的差別眶拉。

將與你的測試無關(guān)的東西隔離開來,通過在頁面或測試的setup設(shè)置中預(yù)先聲明它們憔儿,使它們位于測試計時部分的外面忆植。

與將你的真實代碼限制在很小的一塊,并脫離上下文環(huán)境來進行基準分析相比谒臼,測試與基準分析在它們包含更大的上下文環(huán)境(但仍然有意義)時表現(xiàn)更好朝刊。這些測試將會趨向于運行得更慢,這意味著你發(fā)現(xiàn)的任何差別都在上下文環(huán)境中更有意義蜈缤。

微觀性能

好了拾氓,直至現(xiàn)在我們一直圍繞著微觀性能的問題跳舞,并且一般上不贊成癡迷于它們底哥。我想花一點兒時間直接解決它們咙鞍。

當你考慮對你的代碼進行性能基準分析時房官,第一件需要習(xí)慣的事情就是你寫的代碼不總是引擎實際運行的代碼。我們在第一章中討論編譯器的語句重排時簡單地看過這個話題续滋,但是這里我們將要說明編譯器能有時決定運行與你編寫的不同的代碼翰守,不僅是不同的順序,而是不同的替代品疲酌。

讓我們考慮這段代碼:

var foo = 41;

(function(){
    (function(){
        (function(baz){
            var bar = foo + baz;
            // ..
        })(1);
    })();
})();

你也許會認為在最里面的函數(shù)的foo引用需要做一個三層作用域查詢蜡峰。我們在這個系列叢書的 作用域與閉包 一卷中涵蓋了詞法作用域如何工作,而事實上編譯器通常緩存這樣的查詢徐勃,以至于從不同的作用域引用foo不會實質(zhì)上“花費”任何額外的東西事示。

但是這里有些更深刻的東西需要思考。如果編譯器認識到foo除了這一個位置外沒有被任何其他地方引用僻肖,進而注意到它的值除了這里的41外沒有任何變化會怎么樣呢肖爵?

JS編譯器能夠決定干脆完全移除foo變量,并 內(nèi)聯(lián) 它的值是可能和可接受的臀脏,比如這樣:

(function(){
    (function(){
        (function(baz){
            var bar = 41 + baz;
            // ..
        })(1);
    })();
})();

注意: 當然劝堪,編譯器可能也會對這里的baz變量進行相似的分析和重寫。

但你開始將你的JS代碼作為一種告訴引擎去做什么的提示或建議來考慮揉稚,而不是一種字面上的需求秒啦,你就會理解許多對零碎的語法細節(jié)的癡迷幾乎是毫無根據(jù)的。

另一個例子:

function factorial(n) {
    if (n < 2) return 1;
    return n * factorial( n - 1 );
}

factorial( 5 );     // 120

啊搀玖,一個老式的“階乘”算法余境!你可能會認為JS引擎將會原封不動地運行這段代碼。老實說灌诅,它可能會——但我不是很確定芳来。

但作為一段軼事,用C語言表達的同樣的代碼并使用先進的優(yōu)化處理進行編譯時猜拾,將會導(dǎo)致編譯器認為factorial(5)調(diào)用可以被替換為常數(shù)值120即舌,完全消除這個函數(shù)以及調(diào)用!

另外挎袜,一些引擎有一種稱為“遞歸展開(unrolling recursion)”的行為顽聂,它會意識到你表達的遞歸實際上可以用循環(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)擔心n * factorial(n-1)n *= factorial(--n)哪一個運行的更快紊搪。也許你甚至做了性能基準分析來試著找出哪個更好。但是你忽略了一個事實全景,就是在更大的上下文環(huán)境中耀石,引擎也許不會運行任何一行代碼,因為它可能展開了遞歸蚪燕!

說到--娶牌,--nn--的對比,經(jīng)常被認為可以通過選擇--n的版本進行優(yōu)化馆纳,因為理論上在匯編語言層面的處理上诗良,它要做的努力少一些。

在現(xiàn)代的JavaScript中這種癡迷基本上是沒道理的鲁驶。這種事情應(yī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)當在變量len中緩存數(shù)組x的長度,因為從表面上看它不會改變客冈,來避免在循環(huán)的每一次迭代中都查詢x.length所花的開銷旭从。

如果你圍繞x.length的用法進行性能基準分析,與將它緩存在變量len中的用法進行比較场仲,你會發(fā)現(xiàn)雖然理論聽起來不錯和悦,但是在實踐中任何測量出的差異都是在統(tǒng)計學(xué)上完全沒有意義的。

事實上燎窘,在像v8這樣的引擎中摹闽,可以看到(http://mrale.ph/blog/2014/12/24/array-length-caching.html)通過提前緩存長度而不是讓引擎幫你處理它會使事情稍稍惡化。不要嘗試在聰明上戰(zhàn)勝你的JavaScript引擎褐健,當它來到性能優(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)細節(jié)吵血,并決定如何編寫定制的JS代碼來最大限度的利用v8的工作方式谎替。通過這樣的努力你實際上可以在性能優(yōu)化上達到驚人的高度,所以這種努力的收益可能十分高蹋辅。

一些針對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)化方式進行一下合理性檢驗。

你真的在編寫僅僅需要在一種JS引擎上運行的代碼嗎殿托?即便你的代碼 當前 是完全為了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)細節(jié)有關(guān)莹捡。

結(jié)果鬼吵,當時在業(yè)界廣泛傳播的“最佳實踐”建議開發(fā)者們總是使用數(shù)組join(..)的方式。而且有許多人遵循了篮赢。

但是齿椅,某一天,JS引擎改變了內(nèi)部管理字符串的方式启泣,而且特別在+連接上做了優(yōu)化涣脚。他們并沒有放慢join(..),但是他們在幫助+用法上做了更多的努力寥茫,因為它依然十分普遍遣蚀。

注意: 某些特定方法的標準化和優(yōu)化的實施,很大程度上決定于它被使用的廣泛程度。這經(jīng)常(隱喻地)稱為“paving the cowpath”(不提前做好方案芭梯,而是等到事情發(fā)生了再去應(yīng)對)险耀。

一旦處理字符串和連接的新方式定型,所有在世界上運行的玖喘,使用數(shù)組join(..)來連接字符串的代碼都不幸地變成了次優(yōu)的方式胰耗。

另一個例子:曾經(jīng),Opera瀏覽器在如何處理基本包裝對象的封箱/拆箱(參見本系列的 類型與文法)上與其他瀏覽器不同芒涡。因此他們給開發(fā)者的建議是,如果一個原生string值的屬性(如length)或方法(如charAt(..))需要被訪問卖漫,就使用一個String對象取代它费尽。這個建議也許對那時的Opera是正確的,但是對于同時代的其他瀏覽器來說簡直就是完全相反的羊始,因為它們都對原生string進行了專門的優(yōu)化旱幼,而不是對它們的包裝對象。

我認為即使是對今天的代碼突委,這種種陷阱即便可能性不高柏卤,至少也是可能的。所以對于在我的JS代碼中單純地根據(jù)引擎的實現(xiàn)細節(jié)來進行大范圍的優(yōu)化這件事來說我會非常小心匀油,特別是如果這些細節(jié)僅對一種引擎成立時缘缚。

反過來也有一些事情需要警惕:你不應(yīng)當為了繞過某一種引擎難于處理的地方而改變一塊代碼。

歷史上敌蚜,IE是導(dǎo)致許多這種挫折的領(lǐng)頭羊桥滨,在老版本的IE中曾經(jīng)有許多場景,在當時的其他主流瀏覽器中看起來沒有太多麻煩的性能方面苦苦掙扎弛车。我們剛剛討論的字符串連接在IE6和IE7的年代就是一個真實的問題齐媒,那時候使用join(..)就可能要比使用+能得到更好的性能。

不過為了一種瀏覽器的性能問題而使用一種很有可能在其他所有瀏覽器上是次優(yōu)的編碼方式纷跛,很難說是正當?shù)挠骼ā<幢氵@種瀏覽器占有了你的網(wǎng)站用戶的很大市場份額,編寫恰當?shù)拇a并仰仗瀏覽器最終在更好的優(yōu)化機制上更新自己可能更實際贫奠。

“沒什么是比暫時的黑科技更永恒的唬血。”你現(xiàn)在為了繞過一些性能的Bug而編寫的代碼可能要比這個Bug在瀏覽器中存在的時間長的多叮阅。

在那個瀏覽器每五年才更新一次的年代刁品,這是個很難做的決定。但是如今浩姥,所有的瀏覽器都在快速地更新(雖然移動端的世界還有些滯后)挑随,而且它們都在競爭而使得web優(yōu)化特性變得越來越好。

如果你真的碰到了一個瀏覽器有其他瀏覽器沒有的性能瑕疵勒叠,那么就確保用你一切可用的手段來報告它兜挨。絕大多數(shù)瀏覽器都有為此而公開的Bug追跡系統(tǒng)膏孟。

提示: 我只建議,如果一個在某種瀏覽器中的性能問題真的是極端攪局的問題時才繞過它拌汇,而不是僅僅因為它使人厭煩或沮喪柒桑。而且我會非常小心地檢查這種性能黑科技有沒有在其他瀏覽器中產(chǎn)生負面影響。

大局

與擔心所有這些微觀性能的細節(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在語境中的原話:

程序員們浪費了大量的時間考慮配乓,或者擔心氯庆,他們的程序中的 不關(guān)鍵 部分的速度,而在考慮調(diào)試和維護時這些在效率上的企圖實際上有很強大的負面影響扰付。我們應(yī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īng)該優(yōu)化它盐固!——或者不荒给。

我甚至可以激進地這么說:沒有花在優(yōu)化關(guān)鍵路徑上的時間是浪費的,不管它的效果多么微小刁卜。沒有花在優(yōu)化非關(guān)鍵路徑上的時間是合理的志电,不管它的效果多么大。

如果你的代碼在關(guān)鍵路徑上蛔趴,比如將要一次又一次被運行的“熱”代碼塊兒挑辆,或者在用戶將要注意到的UX關(guān)鍵位置,比如循環(huán)動畫或者CSS樣式更新,那么你應(yīng)當不遺余力地進行有意義的鱼蝉,可測量的重大優(yōu)化洒嗤。

舉個例子,考慮一個動畫循環(huán)的關(guān)鍵路徑魁亦,它需要將一個字符串值轉(zhuǎn)換為一個數(shù)字单寂。這當然有多種方法做到缩举,但是哪一個是最快的呢敬鬓?

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ū)別感興趣的話,可以做一個測試利术。

當你考慮這些不同的選擇時终吼,就像人們說的,“有一個和其他的不一樣氯哮。”parseInt(..)可以工作商佛,但它做的事情多的多——它會解析字符串而不是轉(zhuǎn)換它喉钢。你可能會正確地猜想parseInt(..)是一個更慢的選擇,而你可能應(yīng)當避免使用它良姆。

當然肠虽,如果x可能是一個 需要被解析 的值,比如"42px"(比如CSS樣式查詢)玛追,那么parseInt(..)確實是唯一合適的選擇税课!

Number(..)也是一個函數(shù)調(diào)用。從行為的角度講痊剖,它與+二元操作符是相同的韩玩,但它事實上可能慢一點兒,需要更多的機器指令運轉(zhuǎn)來執(zhí)行這個函數(shù)陆馁。當然找颓,JS引擎也可能識別出了這種行為上的對稱性,而僅僅為你處理Number(..)行為的內(nèi)聯(lián)形式(也就是+x)叮贩!

但是要記住击狮,癡迷于+xx | 0的比較在大多數(shù)情況下都是浪費精力。這是一個微觀性能問題益老,而且你不應(yīng)該讓它使你的程序的可讀性降低彪蓬。

雖然你的程序的關(guān)鍵路徑性能非常重要,但它不是唯一的因素捺萌。在幾種性能上大體相似的選擇中档冬,可讀性應(yīng)當是另一個重要的考量。

尾部調(diào)用優(yōu)化 (TCO)

正如我們早前簡單提到的,ES6包含了一個冒險進入性能世界的具體需求捣郊。它是關(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ì)細節(jié)而簡單地說伟阔,調(diào)用一個新函數(shù)需要保留額外的內(nèi)存來管理調(diào)用棧,它稱為一個“棧幀(stack frame)”掰伸。所以前面的代碼段通常需要同時為baz()皱炉,bar(..),和foo(..)都準備一個棧幀狮鸭。

然而合搅,如果一個支持TCO的引擎可以認識到foo(y+1)調(diào)用位于 尾部位置 意味著bar(..)基本上完成了,那么當調(diào)用foo(..)時歧蕉,它就并沒有必要創(chuàng)建一個新的棧幀灾部,而是可以重復(fù)利用既存的bar(..)的棧幀。這不僅更快惯退,而且也更節(jié)省內(nèi)存梳猪。

在一個簡單的代碼段中,這種優(yōu)化機制沒什么大不了的蒸痹,但是當對付遞歸春弥,特別是當遞歸會造成成百上千的棧幀時,它就變成了 相當有用的技術(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(..)仍然是遞歸的平匈,而且它還是可以進行TCO優(yōu)化的,因為兩個內(nèi)部的fact(..)調(diào)用都在 尾部位置藏古。

注意: 一個需要注意的重點是增炭,TCO盡在尾部調(diào)用實際存在時才會實施。如果你沒用尾部調(diào)用編寫遞歸函數(shù)拧晕,性能機制將仍然退回到普通的棧幀分配隙姿,而且引擎對于這樣的遞歸的調(diào)用棧限制依然有效。許多遞歸函數(shù)可以像我們剛剛展示的factorial(..)那樣重寫厂捞,但是要小心處理細節(jié)输玷。

ES6要求各個引擎實現(xiàn)TCO而不是留給它們自行考慮的原因之一是,由于對調(diào)用棧限制的恐懼靡馁,缺少TCO 實際上趨向于減少特定的算法在JS中使用遞歸實現(xiàn)的機會欲鹏。

如果無論什么情況下引擎缺少TCO只是安靜地退化到性能差一些的方式上,那么它可能不會是ES6需要 要求 的東西奈嘿。但是因為缺乏TCO可能會實際上使特定的程序不現(xiàn)實,所以與其說它只是一種隱藏的實現(xiàn)細節(jié)吞加,不如說它是一個重要的語言特性更合適裙犹。

ES6保證,從現(xiàn)在開始衔憨,JS開發(fā)者們能夠在所有兼容ES6+的瀏覽器上信賴這種優(yōu)化機制叶圃。這是JS性能的一個勝利!

復(fù)習(xí)

有效地對一段代碼進行性能基準分析践图,特別是將它與同樣代碼的另一種寫法相比較來看哪一種方式更快掺冠,需要小心地關(guān)注細節(jié)。

與其運行你自己的統(tǒng)計學(xué)上合法的基準分析邏輯码党,不如使用Benchmark.js庫德崭,它會為你搞定。但要小心你如何編寫測試揖盘,因為太容易構(gòu)建一個看起來合法但實際上有漏洞的測試了——即使是一個微小的區(qū)別也會使結(jié)果歪曲到完全不可靠眉厨。

盡可能多地從不同的環(huán)境中得到盡可能多的測試結(jié)果來消除硬件/設(shè)備偏差很重要。jsPerf.com是一個用于大眾外包性能基準分析測試的神奇網(wǎng)站兽狭。

許多常見的性能測試不幸地癡迷于無關(guān)緊要的微觀性能細節(jié)憾股,比如比較x++++x鹿蜀。編寫好的測試意味著理解如何聚焦大局上關(guān)注的問題,比如在關(guān)鍵路徑上優(yōu)化服球,和避免落入不同JS引擎的實現(xiàn)細節(jié)的陷阱茴恰。

尾部調(diào)用優(yōu)化(TCO)是一個ES6要求的優(yōu)化機制,它會使一些以前在JS中不可能的遞歸模式變得可能斩熊。TCO允許一個位于另一個函數(shù)的 尾部位置 的函數(shù)調(diào)用不需要額外的資源就可以執(zhí)行往枣,這意味著引擎不再需要對遞歸算法的調(diào)用棧深度設(shè)置一個隨意的限制了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末座享,一起剝皮案震驚了整個濱河市婉商,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渣叛,老刑警劉巖丈秩,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異淳衙,居然都是意外死亡蘑秽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門箫攀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肠牲,“玉大人,你說我怎么就攤上這事靴跛∽忽ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵梢睛,是天一觀的道長肥印。 經(jīng)常有香客問我,道長绝葡,這世上最難降的妖魔是什么深碱? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮藏畅,結(jié)果婚禮上敷硅,老公的妹妹穿的比我還像新娘。我一直安慰自己愉阎,他們只是感情好绞蹦,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著榜旦,像睡著了一般坦辟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上章办,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天锉走,我揣著相機與錄音滨彻,去河邊找鬼。 笑死挪蹭,一個胖子當著我的面吹牛亭饵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梁厉,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼辜羊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了词顾?” 一聲冷哼從身側(cè)響起八秃,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肉盹,沒想到半個月后昔驱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡上忍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年骤肛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窍蓝。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡腋颠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吓笙,到底是詐尸還是另有隱情淑玫,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布面睛,位于F島的核電站絮蒿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏侮穿。R本人自食惡果不足惜歌径,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一毁嗦、第九天 我趴在偏房一處隱蔽的房頂上張望亲茅。 院中可真熱鬧,春花似錦狗准、人聲如沸克锣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袭祟。三九已至,卻和暖如春捞附,著一層夾襖步出監(jiān)牢的瞬間巾乳,已是汗流浹背您没。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留胆绊,地道東北人氨鹏。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像压状,于是被迫代替她去往敵國和親仆抵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容