【问题标题】:Why is the "new" keyword so much more efficient than assignment?为什么“new”关键字比赋值高效得多?
【发布时间】:2026-02-06 13:05:01
【问题描述】:

我有两种读取字符串和创建字符对象的方法:

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);

    }
}

当我使用 18,554,760 个字符串运行这些方法时,我得到了截然不同的运行时间。我得到的输出是:

newChar took: 20 ms
justChar took: 41 ms

对于较小的输入(4,638,690 个字符),时间变化不大。

newChar took: 12 ms
justChar took: 13 ms

为什么 new 在这种情况下效率更高?

编辑:

我的基准代码很老套。

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");

【问题讨论】:

  • 请向我们展示您的基准代码。
  • 颠倒你在基准测试中的测试顺序,看看你是否得到相同的行为。
  • 你需要循环的远不止这些。否则,你只会遇到 JVM 的热身效果。您是否尝试过切换测试顺序?
  • ...顺便说一句,我(强烈)建议使用适当的微基准框架来考虑预热和 c。 Criterium 是 Clojure 的黄金标准;对于纯 Java,请尝试 code.google.com/p/caliper
  • 我已经在jmh 中确认了 OP 的结果:对于我的字符串,justChar 需要 38 微秒,newChar 需要 19 微秒。

标签: java performance new-operator


【解决方案1】:

嗯,我不确定 Marko 是否有意复制最初的错误。 TL;博士;新实例未使用,被淘汰。调整基准会反转结果。不要相信有缺陷的基准,向他们学习。

这是 JMH 基准测试:

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 3, time = 1)
@Fork(3)
@State(Scope.Thread)
public class Chars {

    // Source needs to be @State field to avoid constant optimizations
    // on sources. Results need to be sinked into the Blackhole to
    // avoid dead-code elimination
    private String string;

    @Setup
    public void setup() {
        string = "12345678901234567890";
        for (int i = 0; i < 10; i++) {
            string += string;
        }
    }

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

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

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

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

    @GenerateMicroBenchmark
    public void newChar_prim(BlackHole bh) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            char c = new Character(string.charAt(i));
            bh.consume(c);
        }
    }

    @GenerateMicroBenchmark
    public void justChar_prim(BlackHole bh) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            char c = Character.valueOf(string.charAt(i));
            bh.consume(c);
        }
    }
}

...结果如下:

Benchmark                   Mode   Samples         Mean   Mean error    Units
o.s.Chars.justChar          avgt         9       93.051        0.365    us/op
o.s.Chars.justChar_DCE      avgt         9       62.018        0.092    us/op
o.s.Chars.justChar_prim     avgt         9       82.897        0.440    us/op
o.s.Chars.newChar           avgt         9      117.962        4.679    us/op
o.s.Chars.newChar_DCE       avgt         9       25.861        0.102    us/op
o.s.Chars.newChar_prim      avgt         9       41.334        0.183    us/op

DCE 代表“Dead Code Elimination”,这就是原始基准所遭受的痛苦。如果我们消除这种影响,按照 JMH 的方式,它需要我们将值沉入黑洞,分数会反转。因此,回想起来,这似乎表明原始代码中的 new Character() 对 DCE 有重大改进,而 Character.valueOf 并没有那么成功。我不确定我们应该讨论为什么,因为这与实际使用生成的字符的实际用例无关。

您可以从这里在两个方面走得更远:

  • 获取基准方法的程序集以确认上述猜想。见PrintAssembly
  • 使用更多线程运行。随着线程数量的增加,返回缓存字符和实例化新字符之间的差异会减小,从而达到“分配墙”。

UPD:跟进 Marko 的问题,主要影响似乎是消除分配本身,无论是通过 EA 还是 DCE,请参阅 *_prim 测试。

UPD2:查看程序集。与-XX:-DoEscapeAnalysis 相同的运行确认主要影响是由于消除了分配,作为逃逸分析的影响:

Benchmark                   Mode   Samples         Mean   Mean error    Units
o.s.Chars.justChar          avgt         9       94.318        4.525    us/op
o.s.Chars.justChar_DCE      avgt         9       61.993        0.227    us/op
o.s.Chars.justChar_prim     avgt         9       82.824        0.634    us/op
o.s.Chars.newChar           avgt         9      118.862        1.096    us/op
o.s.Chars.newChar_DCE       avgt         9       97.530        2.485    us/op
o.s.Chars.newChar_prim      avgt         9      101.905        1.871    us/op

这证明了原来的 DCE 猜想是不正确的。 EA 是主要贡献者。 DCE 的结果仍然更快,因为我们不支付拆箱成本,并且通常会尊重返回值。然而,基准在这方面是有缺陷的。

【讨论】:

  • 您不能返回单个 Character 实例,因为许多是在循环中产生的。您需要将它们中的每一个都下沉,否则您将接受循环专业化,并移除部分循环。
  • 没错,这让我失去了注意力……但如果我真的想修复基准,我会Character 做一些事情而不让它逃跑。我认为这会更公平,因为 EA 可能是故事的一部分,而不是杂散效应。
  • 您对 EA 的影响是正确的。如果Character 被立即拆箱正确使用(请参阅更新后的帖子),EA 会启动并做出大量改进。但是,在基准测试失败的情况下,DCE 完全消除了这种影响! ;)
  • 我仍然无法将您的结果解释为证明任何 DCE。 justChar 涉及更昂贵的数组查找操作而不是在 newChar 情况下将 char 值复制到附近的堆栈位置的假设仍然成立。请注意,*_prim 变体在两种情况下都添加了相同的常量时间,即charValue 调用。
  • 我不会说“不稳定”,而是“复杂”!这就是我的基准测试背后的全部想法:猫头鹰并不是它们看起来的那样。
【解决方案2】:

TL;DR 部分

好消息

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

坏消息

这样做主要是偶然的,因为您的基准测试存在许多技术缺陷,并且它所暴露的效果可能不是您所想的。

new Character() 方法更快当且仅当HotSpot 的逃逸分析成功证明生成的实例可以安全地分配在堆栈而不是堆上。因此,效果并不像您的问题所暗示的那样普遍。

效果说明

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
  • justCharnewChar 变体中都有一些死代码消除 (DCE) 的证据,但这只是部分的;
  • 使用newChar 变体,添加charValue 没有效果,所以显然它是DCE 的;
  • justCharcharValue确实有效果,所以好像没有被淘汰;
  • DCE 的整体影响较小,newCharUsedjustCharUsed 之间的稳定差异就是证明。

【讨论】:

    最近更新 更多