今天看到一道面试题,i++和++i的效率谁高谁低。
面试题的答案是++i要高一点。
我在网上搜了一圈儿,发现很多回答也都是同一个结论。
如果早个几年,我也会认同这个看法,但现在我负责任的说,这个结论是错的。
i++和++i的效率完全一致,包括i+=1,i=i+1,这几个的效率,完全一致。
来看一段源码:
public void test1() { int i = 0; int x = i++; System.out.println(x); } public void test2() { int i = 0; int x = ++i; System.out.println(x); } public void test3() { int i = 0; i += 1; int x = i; System.out.println(x); } public void test4() { int i = 0; i = i + 1; int x = i; System.out.println(x); }
四个方法里,同时定义了一个i和一个x,共计两个变量。执行的操作,也都是为i增加1,存入到x里。
在字节码里,这四个方法的执行过程,都是14条指令。
// Method descriptor #6 ()V // Stack: 2, Locals: 3 public void test1(); 0 iconst_0 1 istore_1 [i] 2 iload_1 [i] 3 iinc 1 1 [i] 6 istore_2 [x] 7 getstatic java.lang.System.out : java.io.PrintStream [15] 10 iload_2 [x] 11 invokevirtual java.io.PrintStream.println(int) : void [21] 14 return Line numbers: [pc: 0, line: 3] [pc: 2, line: 4] [pc: 7, line: 5] [pc: 14, line: 6] Local variable table: [pc: 0, pc: 15] local: this index: 0 type: DialogTest [pc: 2, pc: 15] local: i index: 1 type: int [pc: 7, pc: 15] local: x index: 2 type: int // Method descriptor #6 ()V // Stack: 2, Locals: 3 public void test2(); 0 iconst_0 1 istore_1 [i] 2 iinc 1 1 [i] 5 iload_1 [i] 6 istore_2 [x] 7 getstatic java.lang.System.out : java.io.PrintStream [15] 10 iload_2 [x] 11 invokevirtual java.io.PrintStream.println(int) : void [21] 14 return Line numbers: [pc: 0, line: 9] [pc: 2, line: 10] [pc: 7, line: 11] [pc: 14, line: 12] Local variable table: [pc: 0, pc: 15] local: this index: 0 type: DialogTest [pc: 2, pc: 15] local: i index: 1 type: int [pc: 7, pc: 15] local: x index: 2 type: int // Method descriptor #6 ()V // Stack: 2, Locals: 3 public void test3(); 0 iconst_0 1 istore_1 [i] 2 iinc 1 1 [i] 5 iload_1 [i] 6 istore_2 [x] 7 getstatic java.lang.System.out : java.io.PrintStream [15] 10 iload_2 [x] 11 invokevirtual java.io.PrintStream.println(int) : void [21] 14 return Line numbers: [pc: 0, line: 15] [pc: 2, line: 16] [pc: 5, line: 17] [pc: 7, line: 18] [pc: 14, line: 19] Local variable table: [pc: 0, pc: 15] local: this index: 0 type: DialogTest [pc: 2, pc: 15] local: i index: 1 type: int [pc: 7, pc: 15] local: x index: 2 type: int // Method descriptor #6 ()V // Stack: 2, Locals: 3 public void test4(); 0 iconst_0 1 istore_1 [i] 2 iinc 1 1 [i] 5 iload_1 [i] 6 istore_2 [x] 7 getstatic java.lang.System.out : java.io.PrintStream [15] 10 iload_2 [x] 11 invokevirtual java.io.PrintStream.println(int) : void [21] 14 return Line numbers: [pc: 0, line: 22] [pc: 2, line: 23] [pc: 5, line: 24] [pc: 7, line: 25] [pc: 14, line: 26] Local variable table: [pc: 0, pc: 15] local: this index: 0 type: DialogTest [pc: 2, pc: 15] local: i index: 1 type: int [pc: 7, pc: 15] local: x index: 2 type: int
核心的执行过程同为6条指令,且指令性质一致,到了这里,可以得出结论,它们的效率是完全一致的。
光是知道这一点,就足够了吗?
重点从来就不是问题本身,而是问题背后涵盖的基础知识——如何读懂java字节码。
以test1方法为例:
// Method descriptor #6 ()V // Stack: 2, Locals: 3 public void test1(); 0 iconst_0 1 istore_1 [i] 2 iload_1 [i] 3 iinc 1 1 [i] 6 istore_2 [x] 7 getstatic java.lang.System.out : java.io.PrintStream [15] 10 iload_2 [x] 11 invokevirtual java.io.PrintStream.println(int) : void [21] 14 return Line numbers: [pc: 0, line: 3] [pc: 2, line: 4] [pc: 7, line: 5] [pc: 14, line: 6] Local variable table: [pc: 0, pc: 15] local: this index: 0 type: DialogTest [pc: 2, pc: 15] local: i index: 1 type: int [pc: 7, pc: 15] local: x index: 2 type: int
看着很容易让人迷惑,首先需要记住两个概念:
1、operand stack 操作数栈,记录每一个新创建的值
2、variable table 变量表,记录着每一个变量的值
这个两个数据结构随着方法体创建和销毁,而在方法体里定义的变量,比如x,则是一个“引用”,或者说“指针”。
了解了以上,很容易就能理解这一行:
// Stack: 2, Locals: 3
定义Stack长度为2,局部变量表长度为3
Stack为2,很好理解,因为有0和1两个值
局部变量明明只有x和i,为什么会是3呢?其实这里面还隐含了一个this,下面的字节码里体现了这一点:
Local variable table: [pc: 0, pc: 15] local: this index: 0 type: DialogTest [pc: 2, pc: 15] local: i index: 1 type: int [pc: 7, pc: 15] local: x index: 2 type: int
所以,test1方法体的结构大致如下所示:
我们来跟着代码一步一步执行。
1、iconst_0:首字母i表示int,const则是表示常量,_0表示值为0。结合起来,就是创建一个int类型的0常量,并且入栈。
如下图所示:
2、istore_1[i]:store相关指令是用于操作变量表的,表示取栈顶指存入变量指定位置,具体来说,即是取栈顶的int值,存入变量表位置为1,i指向的位置
如下图所示:
3、iload_1[i]:load相关指令用于操作栈,表示将变量表里指定的值入栈:
4、iinc 1 1[i]:inc指令代表increase,增加。之后的第一参数1表示变量表位置1,第二个参数1表示数值1。这一句实现了变量的更改:
5、istore_1[x]:应用之前讲解过的store命令的概念,可以知道,这条指令修改了变量表位置2(也就是x)的值:
如此,就完成了一次i++操作。
++i操作的不同之处在于,它调换了步骤3和4。
导致栈顶元素在赋值给x之前,变为了1,如下图所示:
到这里,相信你已经掌握了最基本的字节码阅读理解。下面引用的链接里是jvm指令集: