后自增与前自增 - JavaScript 优化

46

我在浏览Google Code时偶然发现了一个名为JSpeed的项目-用于优化JavaScript。

我注意到其中一种优化是将for循环语句中的i++更改为++i

优化前

for (i=0;i<1;i++) {}

for (var i = 0, j = 0; i < 1000000; i++, j++) {
    if (i == 4) {
        var tmp = i / 2;
    }

    if ((i % 2) == 0) {
        var tmp = i / 2;
        i++;
    }
}
var arr = new Array(1000000);
for (i = 0; i < arr.length; i++) {}

优化后

for(var i=0;i<1;++i){}
for(var i=0,j=0;i<1000000;++i,++j){if(i==4){var tmp=i>>1;}
if((i&1)==0){var tmp=i>>1;i++;}}
var arr=new Array(1000000);for(var i=0,arr_len=arr.length;i<arr_len;++i){}

我知道什么是前置和后置递增,但不知道这如何加快代码的速度?


105
优化是指将所有代码挤在一起使其不可读吗?太聪明了! - ChaosPandion
2
不是的。优化实际上是改进和加速代码中某些部分,使其更有效率、CPU成本更低。将代码挤在一起变得难以阅读可能也被称为打包或缩小文件大小 - 这不是必要的优化,因为解压需要时间。 - mauris
7
自从什么时候解析器不需要解包任何东西了?这里的优化是传输,而不是性能。 - Justin Johnson
1
在许多其他语言/编译器中也是如此。 - bgw
3
实际上进行了优化,除以2的操作被替换为右移操作。 - mins
显示剩余3条评论
9个回答

70
这是我读到并能回答您的问题的内容:“preincrement(++i)将1加到i的值上,然后返回i;相比之下,i++则先返回i,再将其加1,这在理论上会导致创建一个临时变量来存储应用增量操作前i的值。”

它来自:http://physical-thought.blogspot.com/2008/11/pre-vs-post-increment-speed-test.html。据我所知,实践可能因编译器而异。顺便说一下:通过http://home.earthlink.net/~kendrasg/info/js_opt/,您可以了解更多关于JavaScript优化的信息。 - KooiInc
嗨Kooilnc - 是的,通过谷歌搜索看到了那篇博客文章。非常感谢。 - mauris
2
请查看此性能测试:http://jsperf.com/preincrement-vs-postincrement-vs-predecrement-vs-postde/5 - hswner
3
i = 1; i = i++; console.log(i); // 1i = 1; i = ++i; console.log(i); // 2以上代码展示了"i++"与"++i"的不同之处。在第一个示例中,i被分配为1,然后i被自增运算符"i++"所影响。该操作会将i的值暂时存储在另一个地方,并将i的值设置为其当前值加1。然后,i的原始值1被赋回给i,最终结果是i保持为1。而在第二个示例中,i被分配为1,然后i被前置自增运算符"++i"所影响。与"i++"不同,"++i"会直接将i的值增加1,然后将新值2赋给i。因此,最终结果是i被赋值为2。 - abdulwadood
请注意,如果您不使用返回值,则此差异无关紧要。任何体面的编译器都可以判断这种情况并为两者生成相同的代码。特别是在增加循环控制变量时,不使用返回值。 - Barmar

58
这只是一种虚假的优化方式。据我所知,你只能节省一个操作码。如果你想通过这种技术来优化你的代码,那么你走错了方向。而且,大多数编译器/解释器都会为你优化它(参考1)。简而言之,我不会担心。但是,如果你真的很担心,你应该使用i+=1
这是我刚刚进行的快速测试。
var MAX = 1000000, t=0,i=0;

t = (new Date()).getTime();
for ( i=0; i<MAX;i++ ) {}
t = (new Date()).getTime() - t;

console.log(t);

t = (new Date()).getTime();
for ( i=0; i<MAX;++i ) {}
t = (new Date()).getTime() - t;

console.log(t);

t = (new Date()).getTime();
for ( i=0; i<MAX;i+=1 ) {}
t = (new Date()).getTime() - t;

console.log(t);

原始结果

Post    Pre     +=
1071    1073    1060
1065    1048    1051
1070    1065    1060
1090    1070    1060
1070    1063    1068
1066    1060    1064
1053    1063    1054

移除最低和最高值

Post    Pre     +=
1071    ----    1060
1065    ----    ----
1070    1065    1060
----    1070    1060
1070    1063    ----
1066    1060    1064
----    1063    1054

平均数

1068.4  1064.2  1059.6

请注意,这是超过一百万次迭代,平均结果在9毫秒内。考虑到JavaScript中的大多数迭代处理都是在更小的集合(例如DOM容器)上完成,这并不是太大的优化。

9
我的观点是,小数据集(<1000)中的差异微不足道,无法真正区分,而在JavaScript中这种情况更为普遍。通常,在JavaScript中迭代的数据集是DOM集合,这些集合通常包含不到200个成员。即使如此,在这种情况下的瓶颈仍然是DOM,而不是预先优化与后缀优化或+=之间的微小差别。 - Justin Johnson
1
@mauris - "1 op * n 次迭代可能会很多" 只有在绝对情况下才会如此;在任何实际代码中,它只是整个循环的一小部分,因此相对于整个操作来看将是可以忽略不计的。在一个耗时 1 秒的循环中,9 毫秒的差异意味着它并不重要。 - simpleuser
我认为这并不足以证明 i += 1 更好。这些数字太接近了 - 最好像 Sylvian Leroux 那样检查字节码。 - Timmmm

13

理论上,使用后++运算符可能会产生一个临时变量。实际上,JavaScript编译器足够聪明,能够避免这种情况,特别是在如此琐碎的情况下。

例如,让我们考虑以下示例代码:

sh$ cat test.js 
function preInc(){
  for(i=0; i < 10; ++i)
    console.log(i);
}

function postInc(){
  for(i=0; i < 10; i++)
    console.log(i);
}

// force lazy compilation
preInc();
postInc();

在这种情况下,NodeJS中的V8编译器会生成完全相同的字节码(特别是查看操作码39-44中的增量):

sh$ node --version
v8.9.4
sh$ node --print-bytecode test.js | sed -nEe '/(pre|post)Inc/,/^\[/p'
[generating bytecode for function: preInc]
Parameter count 1
Frame size 24
   77 E> 0x1d4ea44cdad6 @    0 : 91                StackCheck 
   87 S> 0x1d4ea44cdad7 @    1 : 02                LdaZero 
   88 E> 0x1d4ea44cdad8 @    2 : 0c 00 03          StaGlobalSloppy [0], [3]
   94 S> 0x1d4ea44cdadb @    5 : 0a 00 05          LdaGlobal [0], [5]
         0x1d4ea44cdade @    8 : 1e fa             Star r0
         0x1d4ea44cdae0 @   10 : 03 0a             LdaSmi [10]
   94 E> 0x1d4ea44cdae2 @   12 : 5b fa 07          TestLessThan r0, [7]
         0x1d4ea44cdae5 @   15 : 86 23             JumpIfFalse [35] (0x1d4ea44cdb08 @ 50)
   83 E> 0x1d4ea44cdae7 @   17 : 91                StackCheck 
  109 S> 0x1d4ea44cdae8 @   18 : 0a 01 0d          LdaGlobal [1], [13]
         0x1d4ea44cdaeb @   21 : 1e f9             Star r1
  117 E> 0x1d4ea44cdaed @   23 : 20 f9 02 0f       LdaNamedProperty r1, [2], [15]
         0x1d4ea44cdaf1 @   27 : 1e fa             Star r0
  121 E> 0x1d4ea44cdaf3 @   29 : 0a 00 05          LdaGlobal [0], [5]
         0x1d4ea44cdaf6 @   32 : 1e f8             Star r2
  117 E> 0x1d4ea44cdaf8 @   34 : 4c fa f9 f8 0b    CallProperty1 r0, r1, r2, [11]
  102 S> 0x1d4ea44cdafd @   39 : 0a 00 05          LdaGlobal [0], [5]
         0x1d4ea44cdb00 @   42 : 41 0a             Inc [10]
  102 E> 0x1d4ea44cdb02 @   44 : 0c 00 08          StaGlobalSloppy [0], [8]
         0x1d4ea44cdb05 @   47 : 77 2a 00          JumpLoop [42], [0] (0x1d4ea44cdadb @ 5)
         0x1d4ea44cdb08 @   50 : 04                LdaUndefined 
  125 S> 0x1d4ea44cdb09 @   51 : 95                Return 
Constant pool (size = 3)
Handler Table (size = 16)
[generating bytecode for function: get]
[generating bytecode for function: postInc]
Parameter count 1
Frame size 24
  144 E> 0x1d4ea44d821e @    0 : 91                StackCheck 
  154 S> 0x1d4ea44d821f @    1 : 02                LdaZero 
  155 E> 0x1d4ea44d8220 @    2 : 0c 00 03          StaGlobalSloppy [0], [3]
  161 S> 0x1d4ea44d8223 @    5 : 0a 00 05          LdaGlobal [0], [5]
         0x1d4ea44d8226 @    8 : 1e fa             Star r0
         0x1d4ea44d8228 @   10 : 03 0a             LdaSmi [10]
  161 E> 0x1d4ea44d822a @   12 : 5b fa 07          TestLessThan r0, [7]
         0x1d4ea44d822d @   15 : 86 23             JumpIfFalse [35] (0x1d4ea44d8250 @ 50)
  150 E> 0x1d4ea44d822f @   17 : 91                StackCheck 
  176 S> 0x1d4ea44d8230 @   18 : 0a 01 0d          LdaGlobal [1], [13]
         0x1d4ea44d8233 @   21 : 1e f9             Star r1
  184 E> 0x1d4ea44d8235 @   23 : 20 f9 02 0f       LdaNamedProperty r1, [2], [15]
         0x1d4ea44d8239 @   27 : 1e fa             Star r0
  188 E> 0x1d4ea44d823b @   29 : 0a 00 05          LdaGlobal [0], [5]
         0x1d4ea44d823e @   32 : 1e f8             Star r2
  184 E> 0x1d4ea44d8240 @   34 : 4c fa f9 f8 0b    CallProperty1 r0, r1, r2, [11]
  168 S> 0x1d4ea44d8245 @   39 : 0a 00 05          LdaGlobal [0], [5]
         0x1d4ea44d8248 @   42 : 41 0a             Inc [10]
  168 E> 0x1d4ea44d824a @   44 : 0c 00 08          StaGlobalSloppy [0], [8]
         0x1d4ea44d824d @   47 : 77 2a 00          JumpLoop [42], [0] (0x1d4ea44d8223 @ 5)
         0x1d4ea44d8250 @   50 : 04                LdaUndefined 
  192 S> 0x1d4ea44d8251 @   51 : 95                Return 
Constant pool (size = 3)
Handler Table (size = 16)

当然,其他JavaScript编译器/解释器可能会有所不同,但这是值得怀疑的。

作为最后一句话,就我所知,无论何时都应该尽可能使用前置递增:因为我经常切换语言,我更喜欢使用正确的语法语义来表达我的意思,而不是依靠编译器的智能。例如,现代C编译器也不会有任何区别。但是在C ++中,对于重载的 operator ++ 可能会产生重大影响。


4

Anatoliy的测试包含在前增量测试函数内的后增量 :(

如果没有这种副作用,以下是结果...

function test_post() {
    console.time('postIncrement');
    var i = 1000000, x = 0;
    do x++; while(i--);
    console.timeEnd('postIncrement');
}

function test_pre() {
    console.time('preIncrement');
    var i = 1000000, x = 0;
    do ++x; while(--i);
    console.timeEnd('preIncrement');
}

test_post();
test_pre();
test_post();
test_pre();
test_post();
test_pre();
test_post();
test_pre();

输出

postIncrement: 3.21ms
preIncrement: 2.4ms
postIncrement: 3.03ms
preIncrement: 2.3ms
postIncrement: 2.53ms
preIncrement: 1.93ms
postIncrement: 2.54ms
preIncrement: 1.9ms

那是很大的区别。

我认为它们不同的原因是因为 while(i--) 必须保存 i 的值,然后减少 i,再检查 i 的先前值以确定循环是否完成。而 while(--i) 不需要做这个额外的工作。在条件测试中使用 i--i++ 是非常不寻常的。当然,在 for 语句的增量操作中可以使用,但在条件测试中不应使用。 - Mike Dunlavey
当你使用--i时,应将其设置为1000001,因为它会更早地结束 :) 当然,这并没有太大的区别。 - Elvedin Hamzagic

3
听起来像是过早优化。当你快完成应用程序时,请检查瓶颈所在,并根据需要进行优化。但如果您想要一个循环性能的详细指南,请查看以下内容:

http://blogs.oracle.com/greimer/entry/best_way_to_code_a

但是你永远不知道由于JS引擎的改进和浏览器之间的差异,这个可能会变得过时。最好的选择是不用担心,除非出现问题。让你的代码易读明了。

编辑:根据this guy的说法,前置和后置的统计学差异微不足道(前置可能更糟糕)。


这更多是关于增量部分而不是访问数组的方式。我知道 for(i=0;i<arr.length;i++) 可能会减慢代码(每次迭代都会调用 arr.length)- 但不知道前置和后置增量的影响。 - mauris
1
我在你的链接中没有看到任何关于前置和后置递增的讨论。 - Taylor Leese
哈!我瞎了。我的链接中没有前置和后置。现在正在检查正确的引用。 - Glenn

2
优化并不是前置和后置递增的问题,而是使用位运算符“shift”和“and”而不是除法和取模。
还有一种优化方法是将JavaScript文件压缩以减小总大小(但这不是运行时优化)。

1
有一些证据表明,预处理和后处理确实会有所不同...这取决于引擎。 - Glenn
1
你能提供一个来源吗?这对我来说没有太多意义。 - Taylor Leese
我知道还有其他的优化方法。但如果这不被认为是优化的一部分,那么为什么JSpeed要包括这种将后缀递增改为前缀递增的更改呢? - mauris
1
该链接没有提及关于前缀和后缀增量的任何内容。 - Taylor Leese
是的,我错了。忽略我说的大部分内容吧。我对于读取某些测试时有所不同还有一些模糊的记忆。 - Glenn

1

这可能是一种装模作样的编程方式。 当你使用不具有任意运算符重载的语言的良好编译器/解释器时,它不应该有任何影响。

这种优化对于C++来说是有意义的。

T x = ...;
++x

可以直接修改一个值,而不必创建新的变量。

T x = ...;
x++

需要在幕后执行某些操作才能创建副本

T x = ...;
T copy;
(copy = T(x), ++x, copy)

对于大型结构类型或在其“复制构造函数”中执行大量计算的类型,这可能会很昂贵。


0

我在Firebug中进行了测试,发现后增量和前增量之间没有任何区别。也许这是其他平台的优化?以下是我用于Firebug测试的代码:

function test_post() {
    console.time('postIncrement');
    var i = 1000000, x = 0;
    do x++; while(i--);
    console.timeEnd('postIncrement');
}

function test_pre() {
    console.time('preIncrement');
    var i = 1000000, x = 0;
    do ++x; while(i--);
    console.timeEnd('preIncrement');
}

test_post();
test_pre();
test_post();
test_pre();
test_post();
test_pre();
test_post();
test_pre();

输出为:

postIncrement: 140ms
preIncrement: 160ms
postIncrement: 136ms
preIncrement: 157ms
postIncrement: 148ms
preIncrement: 137ms
postIncrement: 136ms
preIncrement: 148ms

我已经在Firefox上完成了测试。差别不大。其他答案中给出的理论可能就是答案。感谢你的努力! - mauris
谁会在意速度?除非你的JavaScript正在执行数不清的操作,否则对最终用户来说几乎没什么影响。 - mP.
@mP - 确实如此,但有些浏览器 咳嗽IE... =D - mauris
@mP。也许现在可以用Node.js... - moala

0

使用后置递增会导致堆栈溢出。为什么?开始和结束总是返回相同的值,而不是先进行递增操作。

function reverseString(string = [],start = 0,end = string.length - 1) {  
  if(start >= end) return
  let temp = string[start]
  string[start] = string[end]
  string[end] = temp
  //dont't do this
  //reverseString(string,start++,end--)
  reverseString(string,++start,--end)
  return array
}

let array = ["H","a","n","n","a","h"]
console.log(reverseString(array))


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接