连接字符字面值('x')与单个字符字符串字面值("x")的区别

27
当我需要将一个字符连接到字符串的末尾时,出于性能原因,我应该偏爱s = .... + ']' 而不是s = .... + "]"吗?
我了解数组字符串拼接和字符串构建器,并且我不是在寻求一般情况下如何连接字符串的建议。
我知道有些人会想解释什么是过早优化,以及通常情况下不应该为这样的小事烦恼,请不要这样做...
我之所以问这个问题,是因为从编程风格的角度来看,我更喜欢使用后者,但我觉得第一个应该表现稍微更好,因为它知道正在添加的只是一个单个字符,所以没有必要遍历此单个字符上可能发生的内部循环,就像复制单个字符字符串时可能会出现的那样。
更新:
正如@Scheintod所写,这确实是一个理论性的问题,更多的是与我希望更好地理解java的工作方式有关,而不是任何现实生活中“节省另一个微秒”的场景...也许我应该说得更清楚一些。
我喜欢了解事物“背后的故事”,我发现它有时可以帮助我创建更好的代码...
事实是-我根本没有考虑编译器优化...
我不会指望JIT为我使用StringBuilder而不是String... 因为我(可能是错误的)认为在一方面,StringBuilder比String“重”,但在另一方面更快地构建和修改字符串。因此,我会假设在某些情况下,使用StringBuilder会比使用字符串更低效...(如果不是这种情况,那么整个String类应该已经改变了其实现,以成为StringBuilder的实现,并使用某种内部表示来表示实际的不可变字符串...-或者这就是JIT正在做的事情?-假设对于一般情况,最好不要让开发人员选择... )

如果它会对我的代码产生如此大的改变,那么也许我的问题应该达到那个级别,询问JIT是否这样做是合适的,如果它使用更好会怎样。

也许我应该开始看编译的字节码...[我需要学习如何在Java中做到这一点...]

作为一个附注和为什么我会考虑查看字节码的例子-看看我关于优化ActionScript 2.0-基于字节码的角度-第一部分的相当古老的博客文章,它显示了知道你的代码编译成什么确实可以帮助你编写更好的代码。


1
为什么不试着对此进行基准测试? - André Stannek
2
字符在连接之前会被隐式转换为字符串,这没有区别。你真的在寻求“过早优化”的答案,但你不希望我们告诉你那个... - Stefano Sanfilippo
@Orion 运行时解析字符串是什么? - Stefano Sanfilippo
3
我给这个帖子点赞是因为我认为这是一个有趣的理论问题。我明白作者为什么不想获取针对此问题的实际建议。 - Scheintod
我认为答案涉及到一些代码,编译器会生成两种选择中的一种,并深入挖掘这种情况的影响。这将为我们提供有关Java工作原理的更深入的见解。 - Scheintod
2个回答

30

除了对这个进行剖析,我们还有另一种可能性来获得一些见解。我想集中关注可能的速度差异,而不是再次消除它们的事情。

所以让我们从这个Test类开始:

public class Test {

    // Do not optimize this
    public static volatile String A = "A String";

    public static void main( String [] args ) throws Exception {

        String a1 = A + "B";

        String a2 = A + 'B';

        a1.equals( a2 );

    }

}

我用 javac Test.java 进行了编译(使用 javac -v: javac 1.7.0_55)

使用 javap -c Test.class 我们可以得到:

Compiled from "Test.java"
public class Test {
  public static volatile java.lang.String A;

  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: new           #2                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
       7: getstatic     #4                  // Field A:Ljava/lang/String;
      10: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      13: ldc           #6                  // String B
      15: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      18: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      21: astore_1
      22: new           #2                  // class java/lang/StringBuilder
      25: dup
      26: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
      29: getstatic     #4                  // Field A:Ljava/lang/String;
      32: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      35: bipush        66
      37: invokevirtual #8                  // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
      40: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      43: astore_2
      44: aload_1
      45: aload_2
      46: invokevirtual #9                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      49: pop
      50: return

  static {};
    Code:
       0: ldc           #10                 // String A String
       2: putstatic     #4                  // Field A:Ljava/lang/String;
       5: return
}

我们可以看到,涉及到两个StringBuilder(第4行和第22行)。所以我们首先发现的是,使用+连接Strings实际上与使用StringBuilder相同。

这里我们可以看到的第二件事是,两个StringBuilders都被调用了两次。第一次是为了添加易变的变量(第10行和第32行),第二次是为了添加常量部分(第15行和第37行)。

A +“B”的情况下,append被调用时带有一个 Ljava/lang/String (一个字符串)参数,而在A +'B'的情况下,它被调用时带有一个C(一个字符)参数。

因此,编译器不会将String转换为char,而是保留其原始状态*。

现在查看包含使用的方法的AbstractStringBuilder

public AbstractStringBuilder append(char c) {
    ensureCapacityInternal(count + 1);
    value[count++] = c;
    return this;
}
And(同时)
public AbstractStringBuilder append(String str) {
    if (str == null) str = "null";
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

实际调用的方法是关键。在这里最昂贵的操作肯定是ensureCapacity,但仅当达到限制时(它将旧StringBuffer的char[]复制到新StringBuffer中)。因此,对于两者都是如此,没有真正的区别。

正如可以看到的,还有许多其他操作,但真正的区别在于value[count++] = c;str.getChars(0、len、value、count);

如果我们查看getChars,就会发现它归结为一个System.arrayCopy,该方法用于将字符串复制到缓冲区的数组中,加上一些检查和额外的方法调用,而A + 'B'只需要一次单个数组访问。

因此,我认为从理论上讲,使用A + "B"比使用A + 'B'慢得多

认为在实际执行中也更慢。但要确定这一点,我们需要进行基准测试。

编辑: 当然,这都是在JIT发挥魔力之前。请参见Stephen C的答案。

编辑2: 我一直在查看Eclipse编译器生成的字节码,它几乎相同。因此,至少这两个编译器在结果上没有区别。

编辑2: 现在是有趣的部分

基准测试。这个结果是在热身后运行了几次a+'B'a+"B"循环0..100M后生成的:

a+"B": 5096 ms
a+'B': 4569 ms
a+'B': 4384 ms
a+"B": 5502 ms
a+"B": 5395 ms
a+'B': 4833 ms
a+'B': 4601 ms
a+"B": 5090 ms
a+"B": 4766 ms
a+'B': 4362 ms
a+'B': 4249 ms
a+"B": 5142 ms
a+"B": 5022 ms
a+'B': 4643 ms
a+'B': 5222 ms
a+"B": 5322 ms

平均值为:

a+'B': 4608ms
a+"B": 5167ms

即使在合成知识的真实基准测试世界中(呵呵),a+'B'a+"B"约10%……

...至少(免责声明)在我的系统我的编译器我的CPU上,这真的没有什么区别/不值得注意。除非你有一段代码需要经常运行,而且所有应用程序的性能都取决于它。但那时你可能会首先考虑不同的事情。

编辑4:

思考一下。以下是用于进行基准测试的循环:

    start = System.currentTimeMillis();
    for( int i=0; i<RUNS; i++ ){
        a1 = a + 'B';
    }
    end = System.currentTimeMillis();
    System.out.println( "a+'B': " + (end-start) + " ms" );

所以我们不仅关注一个问题,而是测试Java循环性能、对象创建性能和变量赋值性能。因此,真正的速度差异可能会更大一些。


所以我们真正关心的不只是一个问题,我们还要测试Java循环、对象创建和变量赋值的性能。因此,实际的速度差异可能会更大。

1
谢谢。这正是我在寻找的分析类型。 然而,我不确定如何阅读您最后一条关于JIT的评论...它是否指涉那些可以在编译之前完成连接的情况?还是其他什么?如果A的值是从控制台读取的,您的分析会发生很大变化吗? - epeleg
1
Eclipse有自己的编译器。通常情况下,它生成的类会被放置在某个binbuild目录中,这些目录是在“项目设置->Java构建路径->源->默认输出文件夹”中配置的。使用javap对其进行操作与使用javac生成的.class文件相同。 - Scheintod
1
jit:在编译代码时,并不会进行所有可能的优化,而是在运行代码时进行。由于Java现在使用了一个热点即时编译器(JIT),它只会在需要时进行这些优化。关于具体发生了什么以及可能性,我并不是最适合的人选,我也不知道如何找出来。这里有一点相关信息:https://wikis.oracle.com/display/HotSpotInternals/PerformanceTechniques(无论如何,这都是一篇很好的阅读材料)。 - Scheintod
1
在Stephen C的回答中,“内在”的意思是jit可以做一些不同于所写内容的事情:)请参见此问题:https://dev59.com/f2oy5IYBdhLWcg3wdt8L 如果它真的这样做,我就不知道了。 - Scheintod
来这里是为了弄清楚我写单引号来表示单个字符字符串字面量的习惯是从哪里养成的 :) 绝对加1鼓励。 - Jan Zyka
显示剩余2条评论

3
当我需要将一个字符串与单个字符连接在一起时,是否应该优先选择s = .... + ']'而不是s = .... + "]"以获得更好的性能表现?
实际上这里有两个问题:
Q1:是否存在性能差异?
答案:这取决于...
  • In some cases, possibly yes, depending on the JVM and/or the bytecode compiler. If the bytecode compiler generates a call to StringBuilder.append(char) rather than StringBuilder.append(String) then you would expect the former to be faster. But the JIT compiler could treat these methods as "intrinics" and optimize calls to append(String) with a one character (literal) string.

    In short, you would need to benchmark this on your platform to be sure.

  • In other cases, there is definitely no difference. For example, these two calls will be compiled identical bytecode sequences because the concatenation is a Constant Expression.

        System.out.println("234" + "]");
    
        System.out.println("234" + ']');
    

    This is guaranteed by the JLS.

问题2:您是否应该偏好一种形式而不是另一种形式?

回答:

  • 从一般意义上讲,这很可能是一种过早的优化。仅当您在应用程序级别对代码段进行了分析并确定该代码片段对性能产生了可衡量的影响时,您才应该出于性能原因而偏好一种形式而不是另一种形式。

  • 如果您已经对代码进行了分析,请使用问题1的答案作为指南。

    如果值得尝试优化代码段,则必须在优化后重新运行基准测试/分析,以查看是否有任何差异。 您关于什么最快速...以及您在互联网上阅读过的文章可能是非常错误的。


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