Java. 字符串连接。微基准测试

3

首先我运行了这段代码:

public class Demo  {
    public static void main(String[] args) {
        String x = "x";
        long start = System.currentTimeMillis();

        for (int i = 0; i < 100000; i++)

        {

            x = x.concat("s");

            // x+="k";

        }

        System.out.println(System.currentTimeMillis() - start);
    }

}

输出:13579。

在第二步骤中,我运行了这段代码:

public class Demo {
    public static void main(String[] args) {
        String x = "x";
        long start = System.currentTimeMillis();

        for (int i = 0; i < 100000; i++)

        {

            //x = x.concat("s");

             x+="k";

        }

        System.out.println(System.currentTimeMillis() - start);
    }

}

输出:27328。

我有两个问题:

  1. 我可以说我的基准测试是正确的吗?
  2. 为什么使用(+)和concat()之间有如此大的时间差异?13.5秒VS 27秒。为什么?

你是否运行了多次基准测试以确保它的正确性?我曾经认为 + 和 concat 是完全等价的。但如果你使用 StringBuilder,应该会有很大的区别... - pcalcao
@pcalcao:它们在语义上是等价的。没有人承诺它们会有相同的性能。 - Donal Fellows
5个回答

6
我认为你的微基准测试很好,我可以重复你的结果。
在我的JVM上,“x +=“ k””两倍缓慢的原因是,在底层它执行以下操作:
1.创建一个新的StringBuilder; 2.将x附加到StringBuilder; 3.将“k”附加到StringBuilder; 4.调用StringBuilder.toString()并将结果分配给x。
这将复制字符数据两次(步骤2和步骤4中各一次)。
另一方面,“x = x.concat("s")”只会复制一次数据。
这种双重复制使“x +=“ k””比其他版本慢两倍。
如果你感兴趣,这里是我的编译器为+=循环生成的字节码:
   10:  goto    36
   13:  new #24; //class java/lang/StringBuilder
   16:  dup
   17:  aload_1
   18:  invokestatic    #26; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
   21:  invokespecial   #32; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
   24:  ldc #35; //String k
   26:  invokevirtual   #37; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   29:  invokevirtual   #41; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   32:  astore_1
   33:  iinc    4, 1
   36:  iload   4
   38:  ldc #45; //int 100000
   40:  if_icmplt   13

第21条和第29条是制作两份副本的位置。


好的解释 +1。此外,我们可以说,在虚拟机中需要创建更多的对象,我认为这也可能是导致速度有点慢的原因之一。 - javing

0

现在试试这个,它会击败你们两个:

public class Demo {
    public static void main(String[] args) {
        StringBuilder x = new StringBuilder("x");
        long start = System.currentTimeMillis();

        for (int i = 0; i < 100000; i++)

        {

             x.append("k");

        }

        System.out.println(System.currentTimeMillis() - start);
    }

}

这里是稍微优化过的版本:

public class Demo {
    public static void main(String[] args) {
        StringBuilder x = new StringBuilder(100001).append('x');
        long start = System.currentTimeMillis();

        for (int i = 0; i < 100000; i++)

        {

             x.append('k');

        }

        System.out.println(System.currentTimeMillis() - start);
    }

}

0

如果你知道这些是等价的,会有帮助吗:

x+="k";

x = new StringBuffer(x).append("k").toString();

在更新的Java版本中(我忘记是1.5还是1.6),它使用了这个,因为它至少避免了StringBuffer所带来的锁定问题:

x = new StringBuilder(x).append("k").toString();

由于存在所有这些内存管理,难怪 concat(String) 方法能做得更好。另一方面,如果您要执行该循环,最好不要在可变对象和不可变对象之间频繁转换:

String x = "x";
long start = System.currentTimeMillis();

StringBuilder sb = new StringBuilder(x);
for (int i = 0; i < 100000; i++) {
    sb.append("f");
}
x = sb.toString();

System.out.println(System.currentTimeMillis() - start);

由于 StringBuilder(以及 StringBuffer,但由于其他原因效率较低)会为您进行智能缓冲区管理,因此这将是一种显着更快的技术;要比这更快,需要真正的努力(或者至少在本例中预先调整缓冲区大小,但在一般情况下更难)。


0
因为当您使用+连接字符串时,实际上会创建一个新的StringBuffer并在其上进行操作,而不是原始字符串,这就是为什么最终使用此方法可能会比仅使用concat慢的原因。
关于正确性,最好查看字节码以了解实际发生的情况。如果编译器知道执行结果的确切值并且它永远不会更改,那么编译器可能会优化掉某些代码。

自Java 1.6以来,它是一个新的StringBuilder,而不是StringBuffer。 - Sean Patrick Floyd
感谢您的补充,但是原帖并没有明确说明他正在运行的版本。 - Malcolm

0

如果您单步执行以下行:

    String x2 = x + "x";

在Eclipse中,您会看到它创建了一个StringBuilder对象来帮助构建新的String。这就是发生在您的第二种情况中的事情。

在第一种情况下,String.concat直接创建一个新的String,而没有使用任何StringBuilder,因此速度稍快。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接