为什么 Java 有一个 IINC 字节码指令?

Why does Java have an IINC bytecode instruction?

为什么Java有一个IINC字节码指令? 已经有一个 IADD 字节码指令可以用来完成相同的任务。

那么为什么 IINC 存在?

查看 this table,有几个重要的区别。

iinc: increment local variable #index by signed byte const

  1. iinc 使用 register 而不是堆栈。
  2. iinc 只能递增 signed 字节值。如果你想将 [-128,127] 添加到一个整数,那么你可以使用 iinc,但是一旦你想添加一个超出该范围的数字,你就需要使用 isubiadd,或多个 iinc 指令。

E1:

TL;DR

我基本上是对的,除了限制是有符号的短值(16 位 [-32768,32767])。有一个 wide 字节码指令修改 iinc (和其他一些指令)以使用 16 位数字而不是 8 位数字。

此外,考虑将两个变量加在一起。如果其中一个变量不是常量,编译器将永远无法将其值内联到字节码,因此它不能使用 iinc;它必须使用 iadd.


package SO37056714;

public class IntegerIncrementTest {
  public static void main(String[] args) {
    int i = 1;
    i += 5;
  }
}

我将对上面的代码进行试验。正如预期的那样,它使用 iinc

$ javap -c IntegerIncrementTest.class 
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
  public SO37056714.IntegerIncrementTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iinc          1, 5
       5: return
}

i += 127 按预期使用 iinc

$ javap -c IntegerIncrementTest.class 
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
  public SO37056714.IntegerIncrementTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iinc          1, 127
       5: return
}

i += 128 不再使用 iinc,而是 iinc_w:

$ javap -c IntegerIncrementTest.class 
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
  public SO37056714.IntegerIncrementTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iinc_w        1, 128
       8: return
}

i -= 601 也使用 iinc_w:

$ javap -c IntegerIncrementTest.class 
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
  public SO37056714.IntegerIncrementTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iinc_w        1, -601
       8: return
}

_w 后缀指的是 wide 字节码,它允许常量最多为 16 位 ([-32768, 32767])。

如果我们尝试 i += 32768,我们将看到我上面预测的结果:

$ javap -c IntegerIncrementTest.class 
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
  public SO37056714.IntegerIncrementTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iload_1
       3: ldc           #16                 // int 32768
       5: iadd
       6: istore_1
       7: return
}

此外,考虑我们向 i (i += c) 添加另一个变量的情况。编译器不知道 c 是否常量,所以它不能将 c 的值内联到字节码。对于这种情况,它也会使用 iadd

int i = 1;
byte c = 3;
i += c;
$ javap -c IntegerIncrementTest.class 
Compiled from "IntegerIncrementTest.java"
public class SO37056714.IntegerIncrementTest {
  public SO37056714.IntegerIncrementTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_3
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: istore_1
       8: return
}

只有 Java 的原始设计者可以回答他们为什么做出特定的设计决定。不过,我们可以推测:

IINC 不允许您做任何 ILOAD/SIPUSH/IADD/ISTORE 组合无法完成的事情。不同的是IINC是单条指令,只占用3或6个字节,而4条指令序列明显更长。所以 IINC 稍微减少了使用它的字节码的大小。

除此之外,Java 的早期版本使用解释器,其中每条指令在执行期间都有开销。在这种情况下,使用单个 IINC 指令可能比等效的替代字节码序列更快。请注意,JITting 使这在很大程度上变得无关紧要,但是 IINC 可以追溯到 Java.

的原始版本

因为 单个 iinc 指令比 iloadsipushiaddistore 序列短。还有证据表明,执行常见情况的代码大小缩减是一个重要的动机。

有专门的指令来处理前四个局部变量,例如aload_0aload 0 的作用相同,它经常用于在操作数堆栈上加载 this 引用。有一个 ldc 指令能够引用前 255 个常量池项中的一个,而所有这些项都可以由 ldc_w 处理,分支指令使用两个字节作为偏移量,因此只有过大的方法必须goto_w-15iconst_n 指令存在,尽管这些都可以由 bipush 处理,它支持所有也可以处理的值sipush,可以被 ldc.

取代

所以非对称指令是常态。在典型的应用程序中,有很多只有几个局部变量的小方法,较小的数字比较大的数字更常见。 iinc 直接等效于独立的 i++i+=smallConstantNumber 表达式(应用于局部变量),它们通常出现在循环中。通过能够在不失去表达所有代码的能力的情况下用更紧凑的代码表达通用代码习惯用法,您将大大节省整体代码大小。

正如已经指出的那样,在与 compiled/optimized 代码执行无关的解释执行中只有很小的机会可以更快地执行。