为什么“ new”关键字比赋值效率高得多?


问题内容

我有两种方法可以读取字符串并创建Character对象:

static void newChar(String string) {
    int len = string.length();
    System.out.println("Reading " + len + " characters");
    for (int i = 0; i < len; i++) {
        Character cur = new Character(string.charAt(i));

    }       
}

static void justChar(String string) {
    int len = string.length();
    for (int i = 0; i < len; i++) {
        Character cur = string.charAt(i);

    }
}

当我使用18554760字符串运行方法时,我得到的运行时间截然不同。我得到的输出是:

newChar took: 20 ms
justChar took: 41 ms

使用较小的输入(4,638,690个字符)时,时间没有变化。

newChar took: 12 ms
justChar took: 13 ms

在这种情况下,为什么新的效率更高?

编辑:

我的基准代码很hacky。

start = System.currentTimeMillis();
newChar(largeString);
end = System.currentTimeMillis();
diff = end-start;
System.out.println("New char took: " + diff + " ms");

start = System.currentTimeMillis();
justChar(largeString);
end = System.currentTimeMillis();
diff = end-start;
System.out.println("just char took: " + diff+ " ms");

问题答案:

TL; DR部分

好消息

您的测量确实显示出真实的效果。

坏消息

它之所以这样做是偶然的,因为您的基准测试存在许多技术缺陷,而它所暴露的效果可能并非您所想到的。

当且仅当 HotSpot的转义分析成功证明可以将生成的实例安全地分配到堆栈而不是堆上时,该new Character()方法才会更快。因此,效果不如您的问题所暗示的那么普遍。 __

效果说明

new Character()更快的原因是 引用的局部性 :您的实例在堆栈中,并且通过CPU高速缓存命中对其进行的所有访问。重用缓存的实例时,必须

  1. 访问远程static字段;
  2. 解引用到远程数组;
  3. 将数组条目取消引用到远程Character实例;
  4. 访问char该实例中包含的内容。

每个取消引用都是潜在的CPU高速缓存未命中。此外,它强制将一部分高速缓存重定向到那些远程位置,从而在输入字符串和/或堆栈位置上导致更多的高速缓存未命中。

细节

我已经使用以下代码运行了此代码jmh

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
public class Chars {
  static String string = "12345678901234567890"; static {
    for (int i = 0; i < 10; i++) string += string;
  }

  @GenerateMicroBenchmark
  public void newChar() {
    int len = string.length();
    for (int i = 0; i < len; i++) new Character(string.charAt(i));
  }

  @GenerateMicroBenchmark
  public void justChar() {
    int len = string.length();
    for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
  }
}

这保留了代码的本质,但消除了一些系统性错误,如预热和编译时间。结果如下:

Benchmark              Mode Thr    Cnt  Sec         Mean   Mean error    Units
o.s.Chars.justChar     avgt   1      3    5       39.062        6.587  usec/op
o.s.Chars.newChar      avgt   1      3    5       19.114        0.653  usec/op

这是我对发生的事情的最佳猜测:

  • newChar您创建一个 的实例Character。HotSpot的转义分析可以证明实例永不转义,因此它允许堆栈分配,或者在特殊情况下Character,可以完全取消分配,因为证明来自该数据从未使用过。

  • justChar你涉及查找到Character高速缓存阵列,其中有 一些 成本。

更新

为了回应Aleks的批评,我在基准测试中添加了更多方法。主要效果保持稳定,但是我们获得了有关次优效果的更多细粒度信息。

  @GenerateMicroBenchmark
  public int newCharUsed() {
    int len = string.length(), sum = 0;
    for (int i = 0; i < len; i++) sum += new Character(string.charAt(i));
    return sum;
  }

  @GenerateMicroBenchmark
  public int justCharUsed() {
    int len = string.length(), sum = 0;
    for (int i = 0; i < len; i++) sum += Character.valueOf(string.charAt(i));
    return sum;
  }

  @GenerateMicroBenchmark
  public void newChar() {
    int len = string.length();
    for (int i = 0; i < len; i++) new Character(string.charAt(i));
  }

  @GenerateMicroBenchmark
  public void justChar() {
    int len = string.length();
    for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
  }

  @GenerateMicroBenchmark
  public void newCharValue() {
    int len = string.length();
    for (int i = 0; i < len; i++) new Character(string.charAt(i)).charValue();
  }

  @GenerateMicroBenchmark
  public void justCharValue() {
    int len = string.length();
    for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i)).charValue();
  }

描述:

  • 基本版本是justCharnewChar;
  • ...Value方法将charValue调用添加到基本版本中;
  • ...Used方法会charValue隐式地添加调用,并 使用 该值排除任何死代码消除。

结果:

Benchmark                   Mode Thr    Cnt  Sec         Mean   Mean error    Units
o.s.Chars.justChar          avgt   1      3    1      246.847        5.969  usec/op
o.s.Chars.justCharUsed      avgt   1      3    1      370.031       26.057  usec/op
o.s.Chars.justCharValue     avgt   1      3    1      296.342       60.705  usec/op
o.s.Chars.newChar           avgt   1      3    1      123.302       10.596  usec/op
o.s.Chars.newCharUsed       avgt   1      3    1      172.721        9.055  usec/op
o.s.Chars.newCharValue      avgt   1      3    1      123.040        5.095  usec/op
  • 有证据表明,在和变体中都消除了 某些 死代码(DCE),但这只是部分的;justChar``newChar
  • 对于newChar变体,添加charValue没有任何作用,因此显然是DCE的;
  • justCharcharValue确实有作用,因此似乎没有被消除;
  • newCharUsed和之间的稳定差异可以证明DCE的总体影响较小justCharUsed