[翻译]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.

    尽管基于函数的迭代显得更加便利,它还是比基于循环的迭代要慢一些。每个数组项要关联额外的函数调用是造成速度慢的原因。在所有情况下,基于函数的迭代占用时间是基于循环的迭代的八倍,因此在关注执行时间的情况下它并不是一个合适的办法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值