【问题标题】:String Pool: "Te"+"st" faster than "Test"?字符串池:“Te”+“st”比“Test”快?
【发布时间】:2012-08-22 20:45:51
【问题描述】:

我正在尝试一些关于字符串池的性能基准测试。然而,结果并不出人意料。

我做了3个静态方法

  • perform0() 方法...每次都创建一个新对象
  • perform1() 方法...字符串文字“Test”
  • perform2() 方法...字符串常量表达式“Te”+“st”

我的预期是(1. 最快 -> 3. 最慢)

  1. “测试”因为字符串池
  2. “Te”+“st”,因为字符串池,但比 1 慢,因为 + 运算符
  3. new String(..) 因为没有字符串池。

但基准测试显示“Te”+“st”比“Test”略快。

new String(): 141677000 ns 
"Test"      : 1148000 ns 
"Te"+"st"   : 1059000 ns

new String(): 141253000 ns
"Test"      : 1177000 ns
"Te"+"st"   : 1089000 ns

new String(): 142307000 ns
"Test"      : 1878000 ns
"Te"+"st"   : 1082000 ns

new String(): 142127000 ns
"Test"      : 1155000 ns
"Te"+"st"   : 1078000 ns
...

代码如下:

import java.util.concurrent.TimeUnit;


public class StringPoolPerformance {

    public static long perform0() {
        long start = System.nanoTime();
        for (int i=0; i<1000000; i++) {
            String str = new String("Test");
        }
        return System.nanoTime()-start;
    }

    public static long perform1() {
        long start = System.nanoTime();
        for (int i=0; i<1000000; i++) {
            String str = "Test";
        }
        return System.nanoTime()-start;
    }

    public static long perform2() {
        long start = System.nanoTime();
        for (int i=0; i<1000000; i++) {
            String str = "Te"+"st";
        }
        return System.nanoTime()-start;
    }

    public static void main(String[] args) {
        long time0=0, time1=0, time2=0;
        for (int i=0; i<100; i++) {
            // result
            time0 += perform0();
            time1 += perform1();
            time2 += perform2();
        }

        System.out.println("new String(): " +  time0 + " ns");
        System.out.println("\"Test\"      : " + time1 + " ns");
        System.out.println("\"Te\"+\"st\"   : " + time2 + " ns");
    }
}

有人能解释为什么“Te”+“st”的执行速度比“Test”快吗? JVM在这里做了一些优化吗?谢谢。

【问题讨论】:

  • 编译器不会只是合并两个常量吗?
  • 字符串 "Te","st" 是文字,所以编译器会在优化阶段将它们更改为 "Test" 吗?我不知道
  • 改变你调用perform1和perform2的顺序,看看你是否仍然得到相同的结果。
  • 上下文切换可能会导致线程等待
  • 您可能想阅读以下内容:Anatomy of a flawed microbenchmark

标签: java string concatenation benchmarking pool


【解决方案1】:

"Te" + "st" 是编译器时的常量表达式,因此在运行时的行为与简单的"Test" 没有区别。任何性能损失都发生在尝试编译时,而不是尝试运行时。

这很容易通过使用 javap -c StringPoolPerformance 反汇编您编译的基准类来证明:

public static long perform1();
  Code:
...
   7:   ldc #3; //int 1000000
   9:   if_icmpge   21
   12:  ldc #5; //String Test
   14:  astore_3
   15:  iinc    2, 1
...

public static long perform2();
  Code:
...
   7:   ldc #3; //int 1000000
   9:   if_icmpge   21
   12:  ldc #5; //String Test
   14:  astore_3
   15:  iinc    2, 1
...

方法的字节码完全相同!这是由Java Language Specification, 15.18.1 指定的:

字符串对象是新创建的(第 12.5 节),除非表达式是编译时常量表达式(第 15.28 节)。

您遇到的基准差异可能是由于典型的可变性或您的基准并不完美。看到这个问题:How do I write a correct micro-benchmark in Java?

你违反的一些值得注意的规则:

  1. 您不会丢弃测试内核的“热身”迭代的结果。
  2. 您没有启用 GC 日志记录(当 perform1()总是在创建一百万个对象的测试之后立即运行时尤其相关)。

【讨论】:

  • Te+st 的运行也可能更快,因为 perform1 的优化立即被 perform2 使用(因为它们完全相同)。
【解决方案2】:

也许 JIT 编译器启动,第三个正在执行本机代码。也许串联被移到了循环之外。也许连接永远不会完成,因为变量永远不会被读取。也许区别在于噪音,而您的三个样本恰好指向相同的方向。

Robust Java benchmarking, Part 1: Issues 解释了许多 Java 基准测试可能出错的方法。

基准测试非常困难。许多因素,无论是明显的还是微妙的,都会影响您的结果。为了获得准确的结果,您需要彻底了解这些问题,可能通过使用解决其中一些问题的基准测试框架。转至 Part 2 了解如此强大的 Java 基准测试框架。

在您了解 JVM 架构引入的特定缺陷之前,不要指望 Java 代码的微基准能够告诉您任何有用的信息,也不要期望即使是最好的微基准也能预测实际应用程序的性能。

我不知道你的目标是什么,但是学习使用一个好的分析器并在你的实际应用程序中使用它通常会告诉你有问题的行是否真的是低效率的根源,并让你衡量一个效果代码更改。花时间学习分析器可能比花时间编写和调试微基准更好。

【讨论】:

  • 没有(运行时)连接。
  • @HotLicks,你怎么知道?语言规范不要求编译器在不使用常量表达式时折叠常量表达式。
  • @HotLicks,我知道常量表达式是什么,但不要求javac 在需要表达式且表达式恰好也是常量表达式的上下文中进行常量折叠。
  • 它在 BNF 语法中固有的优先级中。
  • @HotLicks,你是对的(尽管它不是语法固有的)。 15.18.1 String Concatenation operator 明确表示“结果是对字符串对象的引用(新创建的,除非表达式是编译时常量表达式),它是两个操作数字符串的串联”
【解决方案3】:

首先很高兴知道:

如果你一遍又一遍地连接字符串,比如说在一个循环中,那么你就会知道,因为它们是不可变的,所以会不断生成新的字符串。 javac 编译器在内部使用 StringBuffer 来执行此操作 - 例如,您有

String itemList = "";
 itemList=itemList + items[i].description;

在一个循环中。

发生的情况是,在循环中生成了两个对象。一个是StringBuffer-

itemList=new StringBuffer().append(itemList).
      append(items[i].description).toString();

另一个是通过 toString() 分配给 itemList 的字符串。`

来源:http://thought-bytes.blogspot.com/2007/03/java-string-performance.html

我认为这不适用于您的情况。在第一次性能测试中,您总是创建一个新对象,因此创建了 1000000 个String("Test") 对象。在第二个和第三个示例中,只创建了一个被许多引用指向的对象。如前所述:"Te"+"st" 被视为编译器时间常数,差异太小,不能说它比“测试”更快。

【讨论】:

    【解决方案4】:

    很抱歉发布答案,但我无法将其放在评论中以显示该基准有多么缺陷。 在 Linux 上,我更改了顺序并得到:

    顺序很重要。

    new String()   : 123328907 ns
    "Test"         : 1153035 ns
    "Te"+"st"      : 5389377 ns
    "a"+"b"+"c"+"d": 1256918 ns
    

    【讨论】:

      【解决方案5】:

      Mark Peters 是对的,两个 String 常量会毫不留情地连接起来。

      这是因为连接 String 对象所需的复制时间取决于它们的大小。

      现在这些被编译器编译成StringBuffer/StringBuilder对象,你可以通过反编译一个.class文件看到。

      您应该看看这些类,但要注意,当将 StringBuilder 或 StringBuffer 呈现为 String 时,将创建一个新的 String 对象。

      【讨论】:

      • @BartoszKP 好的,我写的太快了,“指数”绝对是一个错误的术语。我编辑我的答案。
      • @BartoszKP(被编辑评论的 5 分钟延迟卡住了)你是对的,“指数”是一个绝对错误的术语!我编辑了我的答案,感谢您关注此类错误。
      猜你喜欢
      • 2013-02-12
      • 2013-05-06
      • 2020-10-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-22
      • 1970-01-01
      • 2021-09-06
      相关资源
      最近更新 更多