[翻譯]High Performance JavaScript(012)

第四章  Algorithms and Flow Control  算法和流程控制

 

    The overall structure of your code is one of the main determinants as to how fast it will execute. Having a very small amount of code doesn't necessarily mean that it will run quickly, and having a large amount of code doesn't necessarily mean that it will run slowly. A lot of the performance impact is directly related to how the code has been organized and how you're attempting to solve a given problem.

    代碼整體結構是執行速度的決定因素之一。代碼量少不一定運行速度快,代碼量多也不一定運行速度慢。性能損失與代碼組織方式和具體問題解決辦法直接相關。


    The techniques in this chapter aren't necessarily unique to JavaScript and are often taught as performance optimizations for other languages. There are some deviations from advice given for other languages, though, as there are many more JavaScript engines to deal with and their quirks need to be considered, but all of the techniques are based on prevailing computer science knowledge.

    本章技術不僅適用於JavaScript也適用於其他語言的性能優化。還有一些爲其他語言提供的建議,還要處理多種JavaScript引擎並考慮它們的差異,但這些技術都以當前計算機科學知識爲基礎。

 

Loops  循環

 

    In most programming languages, the majority of code execution time is spent within loops. Looping over a series of values is one of the most frequently used patterns in programming and as such is also one of the areas where efforts to improve performance must be focused. Understanding the performance impact of loops in JavaScript is especially important, as infinite or long-running loops severely impact the overall user experience.

    在大多數編程語言中,代碼執行時間多數在循環中度過。在一系列編程模式中,循環是最常用的模式之一,因此也是提高性能必須關注的地區之一。理解JavaScript中循環對性能的影響至關重要,因爲死循環或者長時間運行的循環會嚴重影響用戶體驗。

 

Types of Loops  循環的類型

 

    ECMA-262, 3rd Edition, the specification that defines JavaScript's basic syntax and behavior, defines four types of loops. The first is the standard for loop, which shares its syntax with other C-like languages:

    ECMA-263標準第三版規定了JavaScript的基本語法和行爲,定義了四種類型的循環。第一個是標準的for循環,與類C語言使用同樣的語法:

 

for (var i=0; i < 10; i++){
  //loop body
}

    The for loop tends to be the most commonly used JavaScript looping construct. There are four parts to the for loop: initialization, pretest condition, post-execute, and the loop body. When a for loop is encountered, the initialization code is executed first, followed by the pretest condition. If the pretest condition evaluates to true, then the body of the loop is executed. After the body is executed, the post-execute code is run. The perceived encapsulation of the for loop makes it a favorite of developers.

    for循環大概是最常用的JavaScript循環結構。它由四部分組成:初始化體,前測條件,後執行體,循環體。當遇到一個for循環時,初始化體首先執行,然後進入前測條件。如果前測條件的計算結果爲true,則執行循環體。然後運行後執行體。for循環封裝上的直接性是開發者喜歡的原因。

 

    The second type of loop is the while loop. A while loop is a simple pretest loop comprised of a pretest condition and a loop body:

    第二種循環是while循環。while循環是一個簡單的預測試循環,由一個預測試條件和一個循環體構成:

 

var i = 0;
while(i < 10){
  //loop body
  i++;
}

    Before the loop body is executed, the pretest condition is evaluated. If the condition evaluates to true, then the loop body is executed; otherwise, the loop body is skipped. Any for loop can also be written as a while loop and vice versa.

    在循環體執行之前,首先對前測條件進行計算。如果計算結果爲true,那麼就執行循環體;否則循環體將被跳過。任何for循環都可以寫成while循環,反之亦然。

 

    The third type of loop is the do-while loop. A do-while loop is the only post-test loop available in JavaScript and is made up of two parts, the loop body and the post-test condition:

    第三種循環類型是do-while循環。do-while循環是JavaScript中唯一一種後測試的循環,它包括兩部分:循環體和後測試條件體:

 

var i = 0;
do {
  //loop body
} while (i++ < 10);

    In a do-while loop, the loop body is always executed at least once, and the post-test condition determines whether the loop should be executed again.

    在一個do-while循環中,循環體至少運行一次,後測試條件決定循環體是否應再次執行。

 

    The fourth and last loop is the for-in loop. This loop has a very special purpose: it enumerates the named properties of any object. The basic format is as follows:

    第四種也是最後一種循環稱爲for-in循環。此循環有一個非常特殊的用途:它可以枚舉任何對象的命名屬性。其基本格式如下:

 

for (var prop in object){
  //loop body
}

    Each time the loop is executed, the prop variable is filled with the name of another property (a string) that exists on the object until all properties have been returned. The returned properties are both those that exist on the object instance and those inherited through its prototype chain.

    每次循環執行,屬性變量被填充以對象屬性的名字(一個字符串),直到所有的對象屬性遍歷完成才返回。返回的屬性包括對象的實例屬性和它從原型鏈繼承而來的屬性。

 

Loop Performance  循環性能

 

    A constant source of debate regarding loop performance is which loop to use. Of the four loop types provided by JavaScript, only one of them is significantly slower than the others: the for-in loop.

    循環性能爭論的源頭是應當選用哪種循環。在JavaScript提供的四種循環類型中,只有一種循環比其他循環明顯要慢:for-in循環。

 

    Since each iteration through the loop results in a property lookup either on the instance or on a prototype, the for-in loop has considerably more overhead per iteration and is therefore slower than the other loops. For the same number of loop iterations, a for-in loop can end up as much as seven times slower than the other loop types. For this reason, it's recommended to avoid the for-in loop unless your intent is to iterate over an unknown number of object properties. If you have a finite, known list of properties to iterate over, it is faster to use one of the other loop types and use a pattern such as this:

    由於每次迭代操作要搜索實例或原形的屬性,for-in循環每次迭代都要付出更多開銷,所以比其他類型循環慢一些。在同樣的循環迭代操作中,for-in循環比其他類型的循環慢7倍之多。因此推薦的做法如下:除非你需要對數目不詳的對象屬性進行操作,否則避免使用for-in循環。如果你迭代遍歷一個有限的,已知的屬性列表,使用其他循環類型更快,可使用如下模式:

 

var props = ["prop1", "prop2"],
i = 0;
while (i < props.length){
  process(object[props[i]]);
}

    This code creates an array whose members are property names. The while loop is used to iterate over this small number of properties and process the appropriate member on object. Rather than looking up each and every property on object, the code focuses on only the properties of interest, saving loop overhead and time.

    此代碼創建一個由成員和屬性名構成的隊列。while循環用於遍歷這幾個屬性並處理所對應的對象成員,而不是遍歷對象的每個屬性。此代碼只關注感興趣的屬性,節約了循環時間。

 

    Aside from the for-in loop, all other loop types have equivalent performance characteristics such that it's not useful to try to determine which is fastest. The choice of loop type should be based on your requirements rather than performance concerns.

    除for-in循環外,其他循環類型性能相當,難以確定哪種循環更快。選擇循環類型應基於需求而不是性能。

 

    If loop type doesn't contribute to loop performance, then what does? There are actually just two factors:

    如果循環類型與性能無關,那麼如何選擇?其實只有兩個因素:

 

• Work done per iteration

  每次迭代幹什麼
• Number of iterations

  迭代的次數

 

    By decreasing either or both of these, you can positively impact the overall performance of the loop.

    通過減少這兩者中一個或者全部(的執行時間),你可以積極地影響循環的整體性能。

 

Decreasing the work per iteration  減少迭代的工作量

 

    It stands to reason that if a single pass through a loop takes a long time to execute, then multiple passes through the loop will take even longer. Limiting the number of expensive operations done in the loop body is a good way to speed up the entire loop.

    不言而喻,如果一次循環迭代需要較長時間來執行,那麼多次循環將需要更長時間。限制在循環體內進行耗時操作的數量是一個加快循環的好方法。

 

    A typical array-processing loop can be created using any of the three faster loop types. The code is most frequently written as follows:

    一個典型的數組處理循環,可使用三種循環的任何一種。最常用的代碼寫法如下:

 

//original loops
for (var i=0; i < items.length; i++){
  process(items[i]);
}
var j=0;
while (j < items.length){
  process(items[j++]]);
}
var k=0;
do {
  process(items[k++]);
} while (k < items.length);

 

    In each of these loops, there are several operations happening each time the loop body is executed:

    在每個循環中,每次運行循環體都要發生如下幾個操作:

 

1. One property lookup (items.length) in the control condition

   在控制條件中讀一次屬性(items.length)


2. One comparison (i < items.length) in the control condition

   在控制條件中執行一次比較(i < items.length)


3. One comparison to see whether the control condition evaluates to true (i<items.length==true)

   比較操作,察看條件控制體的運算結果是不是true(i < items.length == true)


4. One increment operation (i++)

   一次自加操作(i++)


5. One array lookup (items[i])

   一次數組查找(items[i])


6. One function call (process(items[i]))

   一次函數調用(process(items[i]))

 

    There's a lot going on per iteration of these simple loops, even though there's not much code. The speed at which the code will execute is largely determined by what process() does to each item, but even so, reducing the total number of operations per iteration can greatly improve the overall loop performance.

    在這些簡單的循環中,即使沒有太多的代碼,每次迭代也要進行許多操作。代碼運行速度很大程度上由process()對每個項目的操作所決定,即使如此,減少每次迭代中操作的總數可以大幅度提高循環整體性能。

 

    The first step in optimizing the amount of work in a loop is to minimize the number of object member and array item lookups. As discussed in Chapter 2, these take significantly longer to access in most browsers versus local variables or literal values. The previous examples do a property lookup for items.length each and every time through the loop. Doing so is wasteful, as this value won't change during the execution of the loop and is therefore an unnecessary performance hit. You can improve the loop performance easily by doing the property lookup once, storing the value in a local variable, and then using that variable in the control condition:

    優化循環工作量的第一步是減少對象成員和數組項查找的次數。正如第2章討論的,在大多數瀏覽器上,這些操作比訪問局部變量或直接量需要更長時間。前面的例子中每次循環都查找items.length。這是一種浪費,因爲該值在循環體執行過程中不會改變,因此產生了不必要的性能損失。你可以簡單地將此值存入一個局部變量中,在控制條件中使用這個局部變量,從而提高了循環性能:

 

//minimizing property lookups
for (var i=0, len=items.length; i < len; i++){
  process(items[i]);
}
var j=0,
count = items.length;
while (j < count){
  process(items[j++]]);
}
var k=0,
num = items.length;
do {
  process(items[k++]);
} while (k < num);

 

    Each of these rewritten loops makes a single property lookup for the array length prior to the loop executing. This allows the control condition to be comprised solely of local variables and therefore run much faster. Depending on the length of the array, you can save around 25% off the total loop execution time in most browsers (and up to 50% in Internet Explorer).

    這些重寫後的循環只在循環執行之前對數組長度進行一次屬性查詢。這使得控制條件只有局部變量參與運算,所以速度更快。根據數組的長度,在大多數瀏覽器上你可以節省大約25%的總循環時間(在Internet Explorer可節省50%)。

 

    You can also increase the performance of loops by reversing their order. Frequently, the order in which array items are processed is irrelevant to the task, and so starting at the last item and processing toward the first item is an acceptable alternative. Reversing loop order is a common performance optimization in programming languages but generally isn't very well understood. In JavaScript, reversing a loop does result in a small performance improvement for loops, provided that you eliminate extra operations as a result:

    你還可以通過改變他們的順序提高循環性能。通常,數組元素的處理順序與任務無關,你可以從最後一個開始,直到處理完第一個元素。倒序循環是編程語言中常用的性能優化方法,但一般來說不太容易理解。在JavaScript中,倒序循環可以略微提高循環性能,只要你消除因此而產生的額外操作:

 

//minimizing property lookups and reversing
for (var i=items.length; i--; ){
  process(items[i]);
}
var j = items.length;
while (j--){
  process(items[j]]);
}
var k = items.length-1;
do {
  process(items[k]);
} while (k--);

 

    The loops in this example are reversed and combine the control condition with the decrement operation. Each control condition is now simply a comparison against zero. Control conditions are compared against the value true, and any nonzero number is automatically coerced to true, making zero the equivalent of false. Effectively, the control condition has been changed from two comparisons (is the iterator less than the total and is that equal to true?) to just a single comparison (is the value true?). Cutting down from two comparisons per iteration to one speeds up the loops even further. By reversing loops and minimizing property lookups, you can see execution times that are up to 50%–60% faster than the original.

    例子中使用了倒序循環,並在控制條件中使用了減法。每個控制條件只是簡單地與零進行比較。控制條件與true值進行比較,任何非零數字自動強制轉換爲true,而零等同於false。實際上,控制條件已經從兩次比較(迭代少於總數嗎?它等於true嗎?)減少到一次比較(它等於true嗎?)。將每個迭代中兩次比較減少到一次可以大幅度提高循環速度。通過倒序循環和最小化屬性查詢,你可以看到執行速度比原始版本快了50%-60%。

 

    As a comparison to the originals, here are the operations being performed per iteration for these loops:

    與原始版本相比,每次迭代中只進行如下操作:

 

1. One comparison (i == true) in the control condition

   在控制條件中進行一次比較(i == true)


2. One decrement operation (i--)

   一次減法操作(i--)


3. One array lookup (items[i])

   一次數組查詢(items[i])


4. One function call (process(items[i]))

   一次函數調用(process(items[i]))

 

    The new loop code has two fewer operations per iteration, which can lead to increasing performance gains as the number of iterations increases.

    新循環代碼每次迭代中減少兩個操作,隨着迭代次數的增長,性能將顯著提升。

 

Decreasing the number of iterations  減少迭代次數

 

    Even the fastest code in a loop body will add up when iterated thousands of times. Additionally, there is a small amount of performance overhead associated with executing a loop body, which just adds to the overall execution time. Decreasing the number of iterations throughout the loop can therefore lead to greater performance gains. The most well known approach to limiting loop iterations is a pattern called Duff's Device.

    即使循環體中最快的代碼,累計迭代上千次(也將是不小的負擔)。此外,每次運行循環體時都會產生一個很小的性能開銷,也會增加總的運行時間。減少循環的迭代次數可獲得顯著的性能提升。最廣爲人知的限制循環迭代次數的模式稱作“達夫設備”。

 

    Duff's Device is a technique of unrolling loop bodies so that each iteration actually does the job of many iterations. Jeff Greenberg is credited with the first published port of Duff's Device to JavaScript from its original implementation in C. A typical implementation looks like this:

    達夫設備是一個循環體展開技術,在一次迭代中實際上執行了多次迭代操作。Jeff Greenberg被認爲是將達夫循環從原始的C實現移植到JavaScript中的第一人。一個典型的實現如下:

 

//credit: Jeff Greenberg
var iterations = Math.floor(items.length / 8),
startAt = items.length % 8,
i = 0;
do {
  switch(startAt){
    case 0: process(items[i++]);
    case 7: process(items[i++]);
    case 6: process(items[i++]);
    case 5: process(items[i++]);
    case 4: process(items[i++]);
    case 3: process(items[i++]);
    case 2: process(items[i++]);
    case 1: process(items[i++]);
  }
  startAt = 0;
} while (--iterations);

 

    The basic idea behind this Duff's Device implementation is that each trip through the loop is allowed a maximum of eight calls to process(). The number of iterations through the loop is determined by dividing the total number of items by eight. Because not all numbers are evenly divisible by eight, the startAt variable holds the remainder and indicates how many calls to process() will occur in the first trip through the loop. If there were 12 items, then the first trip through the loop would call process() 4 times, and then the second trip would call process() 8 times, for a total of two trips through the loop instead of 12.

    達夫設備背後的基本理念是:每次循環中最多可8次調用process()函數。循環迭代次數爲元素總數除以8。因爲總數不一定是8的整數倍,所以startAt變量存放餘數,指出第一次循環中應當執行多少次process()。比方說現在有12個元素,那麼第一次循環將調用process()4次,第二次循環調用process()8次,用2次循環代替了12次循環。

 

    A slightly faster version of this algorithm removes the switch statement and separates the remainder processing from the main processing:

    此算法一個稍快的版本取消了switch表達式,將餘數處理與主循環分開:

 

//credit: Jeff Greenberg
var i = items.length % 8;
while(i){
  process(items[i--]);
}
i = Math.floor(items.length / 8);
while(i){
  process(items[i--]);
  process(items[i--]);
  process(items[i--]);
  process(items[i--]);
  process(items[i--]);
  process(items[i--]);
  process(items[i--]);
  process(items[i--]);
}

 

    Even though this implementation is now two loops instead of one, it runs faster than the original by removing the switch statement from the loop body.

    雖然此代碼中使用兩個循環替代了先前的一個,但它去掉了循環體中的switch表達式,速度更快。

 

    Whether or not it's worthwhile to use Duff's Device, either the original or the modified version, depends largely on the number of iterations you're already doing. In cases where the loop iterations are less than 1,000, you're likely to see only an insignificant amount of performance improvement over using a regular loop construct. As the number of iterations increases past 1,000, however, the efficacy of Duff's Device increases significantly. At 500,000 iterations, for instance, the execution time is up to 70% less than a regular loop.

    是否值得使用達夫設備,無論是原始的版本還是修改後的版本,很大程度上依賴於迭代的次數。如果循環迭代次數少於1'000次,你可能只看到它與普通循環相比只有微不足道的性能提升。如果迭代次數超過1'000次,達夫設備的效率將明顯提升。例如500'000次迭代中,運行時間比普通循環減少到70%。

 

Function-Based Iteration  基於函數的迭代

 

    The fourth edition of ECMA-262 introduced a new method on the native array object call forEach(). This method iterates over the members of an array and runs a function on each. The function to be run on each item is passed into forEach() as an argument and will receive three arguments when called, which are the array item value, the index of the array item, and the array itself. The following is an example usage:

    ECMA-262標準第四版介紹了本地數組對象的一個新方法forEach()。此方法遍歷一個數組的所有成員,並在每個成員上執行一個函數。在每個元素上執行的函數作爲forEach()的參數傳進去,並在調用時接收三個參數,它們是:數組項的值,數組項的索引,和數組自身。下面是用法舉例:

 

items.forEach(function(value, index, array){
  process(value);
});

    The forEach() method is implemented natively in Firefox, Chrome, and Safari. Additionally, most JavaScript libraries have the logical equivalent:

    forEach()函數在Firefox,Chrome,和Safari中爲原生函數。另外,大多數JavaScript庫都有等價實現:

 

//YUI 3
Y.Array.each(items, function(value, index, array){
  process(value);
});
//jQuery
jQuery.each(items, function(index, value){
  process(value);
});
//Dojo
dojo.forEach(items, function(value, index, array){
  process(value);
});
//Prototype
items.each(function(value, index){
  process(value);
});
//MooTools
$each(items, function(value, index){
  process(value);
});

    Even though function-based iteration represents a more convenient method of iteration, it is also quite a bit slower than loop-based iteration. The slowdown can be accounted for by the overhead associated with an extra method being called on each array item. In all cases, function-based iteration takes up to eight times as long as loop-based iteration and therefore isn't a suitable approach when execution time is a significant concern.

    儘管基於函數的迭代顯得更加便利,它還是比基於循環的迭代要慢一些。每個數組項要關聯額外的函數調用是造成速度慢的原因。在所有情況下,基於函數的迭代佔用時間是基於循環的迭代的八倍,因此在關注執行時間的情況下它並不是一個合適的辦法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章