摘要:移植到中的一個典型的達夫設備的例子為一個很長很長的數組。但是達夫設備最初這種詭異的寫法和思路,還是驚艷了很多人的,值得我們思考。高性能閱讀簡記一高性能閱讀簡記二高性能閱讀簡記三
四、Aligorithms and Flow Control 算法和流程控制 1、Loops 循環
a、避免使用for/in循環
在JavaScript標準中,有四種類型循環。for、for/in、while、do/while,其中唯一一個性能比其他明顯慢的是for/in。對于for/in循環的使用場景,更多的是針對不確定內部結構的對象的循環。for/in會枚舉對象的命名屬性,只有完全遍歷對象的所有屬性之后包括實例屬性和從原型鏈繼承的屬性,循環才會返回。正因為for/in循環需要搜索實例或者原型的屬性,所以for/in的性能要差很多,因此我們需要盡量避免使用for/in循環,對于那些已知屬性列表的對象,更需要避免使用for/in。
b、Decreasing the work per iteration 減少迭代的工作量
一個標準的for循環組成:
(初始化體; 前側條件/控制條件; 后執行體){ 循環體; }
可能對于單次循環操作,我們所做的性能優化看起來沒什么用,但是對于多次循環,這些性能優化加起來是很明顯的。
for (var i = 0; i < items.length; i++){ eventHandler(items[i]); }
對于上面這樣一個經典的for循環,它的單次操作中,需要做這些工作:
①在控制條件中讀一次屬性(items.length) ②在控制條件中進行一次比較(i < items.length) ③比較操作,判斷條件控制體的結果是否為true(i < items.length == true) ④一次自加操作(i++) ⑤一次數組查找(items[i]) ⑥一次函數調用(eventHandler(items[i]);)
大家都知道的一個優化就是在第一步,每一次的循環中都會查詢一次items.length,這個操作會首先查找items,然后計算長度,一方面,查找items時的性能開銷是浪費的,另一方面,訪問一個局部變量或者是字面量,顯然更快。因此在這里,我們可以通過一個變量緩存items.length,對于較長的數組,可以節約25%的總循環時間(ie中可達到50%)。
另一種提升循環體性能的方法是改變循環的順序,這常用于數組元素的處理順序和任務無關的情況,從最后一個元素開始,直到處理完第一個元素。
for(var i = items.length; i--){ eventHandler(items[i]); }
在一個for循環中,可以省略初始化體和后執行體,這里省略了后執行體,也就是當i -- 之后, i!= flase,則執行eventHandler(items[i]);這里的i是i--之后的值。這里優化地方是將我們前面說的2、3優化了成一步,i是否是true;如果是則執行i--,也就是i = i - 1;
進行這兩方面的優化后,循環體的性能會得到顯著提升。
c、Decreasing the number of iterations 減少迭代次數
除了在設計循環之前周密考慮,使用最優的循環模式,減少迭代次數,另一個減少迭代次數的有名的方法是達夫設備(Duff"s Device),達夫設備最早出現于C中,他的設計理念,是將整個循環每8個一份分成o份并取余數p,第一次循環執行n次循環體,然后執行m次循環,每次循環中執行8次循環體中的操作,這樣原本是m * 8 + n 次循環就變成了m + 1次循環。這對于那些循環體耗時很短的循環來講,降低了在判斷條件上浪費的時間,從而提升性能。移植到javascript中的一個典型的達夫設備的例子:
var m = [1,2,3,...]; //為一個很長很長的數組。 var o = Math.floor(m.length/8); var p = m.length % 8 ; var i = 0; do{ switch(p){ case 0 : console.log(m[i++]); case 7 : console.log(m[i++]); case 6 : console.log(m[i++]); case 5 : console.log(m[i++]); case 4 : console.log(m[i++]); case 3 : console.log(m[i++]); case 2 : console.log(m[i++]); case 1 : console.log(m[i++]); } p = 0; }while(--o);
書上的達夫設備的代碼如上,但是在我看來這段代碼是有問題的,除非m也就是初始循環次數是8的整倍數,否則循環會少執行一輪,也就是8次。不過沒有找到這本書的勘誤,自行完善了一下這里的代碼:
var m = [1,2,3,...]; var o = Math.floor(m.length/8); var p = m.length % 8 ; p === 0 ? "" : o++; var i = 0; do{ switch(p){ case 0 : console.log(m[i++]); case 7 : console.log(m[i++]); case 6 : console.log(m[i++]); case 5 : console.log(m[i++]); case 4 : console.log(m[i++]); case 3 : console.log(m[i++]); case 2 : console.log(m[i++]); case 1 : console.log(m[i++]); } p = 0; }while(--o);
理解上面的代碼需要首先明確一點,不管是在C中還是JavaScript中,如果switch語句中沒有break,則會在匹配到第一個case并執行后執行下一個case中的操作,不管下一個case是否匹配,直到遇到break或者結束的大括號,當然,return也可以。
上面switch版本的達夫設備的改進版是去掉了switch而變得更快,書中的代碼是個死循環(難道因為我看的是pdf版本的有誤,原書是對的嗎),就不貼出來禍害人了,我這梳理后重寫的代碼如下:
var m = [1,2,3,...]; var p = m.length % 8 ; while(p){ console.log(m[--p]); } var i = m.length; var o = Math.floor(m.length/8); while(o--){ console.log(m[--i]); console.log(m[--i]); console.log(m[--i]); console.log(m[--i]); console.log(m[--i]); console.log(m[--i]); console.log(m[--i]); console.log(m[--i]); }
這個版本中的達夫設備將主循環和余數處理的部分分開,并將原數組進行倒序處理。代碼很易懂就不多說。
在實測中,達夫設備的性能并不比傳統的for循環快多少,甚至普遍會慢那么一點點(FireFox不管處理什么樣的循環,都比Chrome慢三倍,這個必須吐槽一下),這是因為在現代瀏覽器中,隨著設備性能的提升,瀏覽器的實現對循環的算法優化的越來越好,在瀏覽器內部處理循環時也會采用自己獨特的算法提升循環的性能,編程時達夫設備帶來的性能提升已經慢慢的變得不足為道;加上達夫設備這種寫法,對于代碼可讀性很不友好,因此現在已經慢慢越來越少會有人采用這樣的方式來做性能優化。但是達夫設備最初這種詭異的寫法和思路,還是驚艷了很多人的,值得我們思考。
在多數現代瀏覽器的實現中,forEach可作為一個原生的方法去使用,此方法相當于遍歷數組的所有成員,并在每個成員上執行一個函數,每個成員上執行的函數作為forEach()的參數傳進去。這種情況下,每一個數組成員都被掛載了一個函數,在執行迭代時調用,這種基于函數的迭代比基于循環的迭代要慢很多,在實測中,會慢20%左右。復雜的函數處理的時候,性能上的問題會更突出。
3、Conditionals 條件表達式a、if-else Versus switch if-else與switch比較
大家約定俗稱的一點是,在條件數量較少時傾向于使用if-else,在條件數量較大時使用switch,不管從代碼可讀性考慮,還是從性能方面考慮,這種做法都是正確的。盡管在實際上,較少條件數量時,使用switch多數情況下也比if-else快,但也只是快的微不足道,因此這種約定俗稱的使用方式是沒有問題的。
b、Optimzing if-else 優化if-else
if-else決定了JavaScript運行流的走向,讓JavaScript運行流盡快找到運行條件并運行顯然會提高函數的執行效率,因此在有多個條件數量時,讓最可能出現的條件排在前面。例如,用js設置中獎概率,一等獎概率10%,二等獎概率20%;三等獎概率30%;不中獎概率40%;更多人的習慣寫法是:
var result = Math.random() * 10; if(result <= 1){ //一等獎 }else if(result > 1 && result <= 3){ //二等獎 }else if(result > 3 && result <= 6){ //三等獎 }else{ //不中獎 }
實際上,最可能出現的是不中獎,但是每次在判斷為不中獎之前需要先進行前三次判斷,此時可以做的優化就是將上述的寫法反過來:
var result = Math.random() * 10; if(result <= 4){ //不中獎 }else if(result > 4 && result <= 7){ //三等獎 }else if(result > 7 && result <= 9){ //二等獎 }else{ //一等獎 }
當然,較真性能的話,這里用switch更好,不過我們考慮的是優化if-else的性能。
另外一種減少條件判斷的長度的辦法是將并列的if-else判斷,組織成嵌套的if-else減少平均的條件判斷長度,例如下面的例子:
var result = Math.floor(Math.random() * 10); if(result === 0){ return 0; }else if(result === 1){ return 1; }else if(result === 2){ return 2; }else if(result === 3){ return 3; }else if(result === 4){ return 4; }else if(result === 5){ return 5; }else if(result === 6){ return 6; }else if(result === 7){ return 7; }else if(result === 8){ return 8; }else if(result === 9){ return 9; }
這時候計算條件體的最大數目是9,我們可以通過嵌套判斷的辦法減少計算判斷體的數目:
if(result < 6){ if(result < 3){ if(result === 0){ return 0; }else if(result === 1){ return 1; }else{ return 2; } }else{ if(result === 3){ return 3; }else if(result === 4){ return 4; }else{ return 5; } } }else{ if(result < 8){ if(result === 6){ return 6; }else{ return 7; } }else{ if(result === 8){ return 8; }else{ return 9; } } }
看起來代碼是多了,但是最大的條件判斷數變成了4,一定程度上提升了性能。當然,這種情況下,一般會使用swtich處理的。
c、Lookup Tables 查表法
當有大量的離散值需要測試時,使用if-else或者switch不論在可讀性上和性能上都不應該去選擇,比如下面的情況:
var array = [0,1,2,3,4,5,6,7...]; switch(result){ case 0: return array[0]; case 1: return array[1]; case 2: return array[2]; case 3: return array[3]; case 4: return array[4]; case 5: return array[5]; case 6: return array[6]; ... }
當數組有數十個上百個數據時,switch語句會是一段很龐大的代碼。這時候可以使用查表法:
var array = [0,1,2,3,4,5,6,7...]; return array[result];
查表法一般適用于數據量稍大的場合,在實際編程中,還是經常會用到這種方法的。
d、Recursion 遞歸
某些場合,比如說階乘函數,遞歸調用無疑是最優的實現方式:
function calc(n){ if(n === 0){ return 1; }else{ return n * calc(n-1); } }
e、Memoization 制表
制表的原理是通過緩存已經運行的計算結果,避免后續的重復計算從而提升性能。也常用于遞歸運算中,例如上面的階乘函數的調用:
var a = calc(10); var b = calc(9); var c = calc(8);
在calc(10)被調用時,就已經計算過了calc(9)和calc(8)的值,這里calc(9)就重復計算了兩次,而calc(8)重復計算了三次,我們可以通過緩存計算結果的辦法去優化:
function m(n){ if(!m.c){ m.c = { "0": 1, "1": 1 }; } if(!m.c.hasOwnProperty(n)){ m.c[n] = n * m(n-1); } return m.c[n]; } var e = m(10); var f = m(9); var g = m(8);
優化后的函數中,m(9)和m(8)并沒有再去計算,從而避免了重復計算。
高性能JavaScript閱讀簡記(一)
高性能JavaScript閱讀簡記(二)
高性能JavaScript閱讀簡記(三)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/51139.html
摘要:移植到中的一個典型的達夫設備的例子為一個很長很長的數組。但是達夫設備最初這種詭異的寫法和思路,還是驚艷了很多人的,值得我們思考。高性能閱讀簡記一高性能閱讀簡記二高性能閱讀簡記三 四、Aligorithms and Flow Control 算法和流程控制 1、Loops 循環 a、避免使用for/in循環在JavaScript標準中,有四種類型循環。for、for/in、while、...
摘要:移植到中的一個典型的達夫設備的例子為一個很長很長的數組。但是達夫設備最初這種詭異的寫法和思路,還是驚艷了很多人的,值得我們思考。高性能閱讀簡記一高性能閱讀簡記二高性能閱讀簡記三 四、Aligorithms and Flow Control 算法和流程控制 1、Loops 循環 a、避免使用for/in循環在JavaScript標準中,有四種類型循環。for、for/in、while、...
摘要:對于直接量和局部變量的訪問性能差異微不足道,性能消耗代價高一些的是全局變量數組項對象成員。當一個函數被創建后,作用域鏈中被放入可訪問的對象。同樣會改變作用域鏈,帶來性能問題。 早前閱讀高性能JavaScript一書所做筆記。 一、Loading and Execution 加載和運行 從加載和運行角度優化,源于JavaScript運行會阻塞UI更新,JavaScript腳本的下載、解析...
摘要:對于直接量和局部變量的訪問性能差異微不足道,性能消耗代價高一些的是全局變量數組項對象成員。當一個函數被創建后,作用域鏈中被放入可訪問的對象。同樣會改變作用域鏈,帶來性能問題。 早前閱讀高性能JavaScript一書所做筆記。 一、Loading and Execution 加載和運行 從加載和運行角度優化,源于JavaScript運行會阻塞UI更新,JavaScript腳本的下載、解析...
摘要:訪問集合元素時使用局部變量對于任何類型的訪問,如果對同一個屬性或者方法訪問多次,最好使用一個局部變量對此成員進行緩存。 三、DOM Scripting DOM編程 我們都知道對DOM操作的代價昂貴,這往往成為網頁應用中的性能瓶頸。在解決這個問題之前,我們需要先知道什么是DOM,為什么他會很慢。 DOM in the Browser World 瀏覽器中的DOM DOM是一個獨立于語言...
閱讀 1649·2021-11-16 11:44
閱讀 2393·2021-10-11 11:07
閱讀 4036·2021-10-09 09:41
閱讀 663·2021-09-22 15:52
閱讀 3187·2021-09-09 09:33
閱讀 2701·2019-08-30 15:55
閱讀 2284·2019-08-30 15:55
閱讀 837·2019-08-30 15:55