连接字符文本 ('x') 与单字符字符串文本 (“x”)

2022-09-01 17:30:33

当我有一个字符串需要将单个字符连接到其末尾时,出于任何性能原因,我应该更喜欢吗?s = .... + ']'s = .... + "]"

我知道数组字符串连接和字符串生成器,我不是在寻求有关如何连接字符串的建议。

我也知道有些人会有冲动向我解释过早优化,一般来说,我不应该打扰这些小事情,请不要......

我之所以问这个问题,是因为从编码风格偏好来看,我更喜欢使用后者,但我觉得第一个应该表现得稍微好一点,因为知道所追加的内容只是一个字符,因此不需要像复制单个字符串时那样对单个字符进行任何内部循环。

更新

正如@Scheintod所写的,这确实是一个理论上的Q,并且必须做更多的事情,因为我的愿望是更好地了解java是如何工作的,而不是任何现实生活中的“让我们再节省一微秒”的情况......也许我应该更清楚地说出来。

我喜欢了解“幕后”的工作方式,我发现它有时可以帮助我创建更好的代码......

事实 - 我根本没有考虑编译器优化...

我不会期望JIT为我使用s而不是s...因为我(可能错误地)认为String构建器一方面比Strings“更重”,但另一方面在构建和修改字符串方面更快。因此,我认为在某些情况下使用s会比使用stings更有效......(如果不是这种情况,那么整个String类应该将其实现更改为a的实现,并对实际的不可变字符串使用一些内部表示... - 或者这就是JIT正在做的事情? - 假设对于一般情况,最好不要让开发人员选择...StringBuilderStringStringBuilderStringBuilder

如果它确实将我的代码更改到这样的程度,那么也许My Q应该处于该级别,询问JIT是否适合做这样的事情,如果使用它会更好。

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

作为一个旁注和例子,为什么我甚至会考虑看字节码 - 看看我关于优化Actionscript 2.0的一篇相当古老的博客文章 - 字节码视角 - 第一部分它表明,知道你的代码编译成什么确实可以帮助你写出更好的代码。


答案 1

除了分析这一点之外,我们还有另一种可能性来获得一些见解。我想把重点放在可能的速度差异上,而不是再次删除它们的东西上。

因此,让我们从这个类开始: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行)。因此,我们发现的第一件事是,使用concat实际上与使用StringBuilder相同。+Strings

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

如果 是 用 (字符串) 参数调用 的,而在 它的情况下用 (char) 参数调用。A + "B"appendLjava/lang/StringA + 'B'C

因此,编译不会将 String 转换为 char,而是保持原样*。

现在查看其中包含我们使用的方法:AbstractStringBuilder

public AbstractStringBuilder append(char c) {
    ensureCapacityInternal(count + 1);
    value[count++] = c;
    return this;
}

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

作为实际调用的方法。

这里最昂贵的操作当然是,但只有在达到限制的情况下(它会将旧的StringBuffers char[]的数组复制到新的数组中)。因此,这对两者都是正确的,并没有真正的区别。ensureCapacity

正如人们所看到的,还有许多其他操作已经完成,但真正的区别在于value[count++] = c;str.getChars(0, len, value, count);

如果我们查看getChars,我们会看到,它归结为一个在这里用于将String复制到缓冲区数组的数组,加上一些检查和其他方法调用,而不是单个数组访问。System.arrayCopy

所以我想说,从理论上讲,使用比使用慢得多A + "B"A + 'B'

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

编辑:因为这只是在JIT做魔术之前的全部。看看斯蒂芬·C对此的回答。

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

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

基准测试。此结果是通过在预热后运行循环 0..100M 和几次生成的:a+'B'a+"B"

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"

...至少(免责声明)在我的系统上使用我的编译器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循环性能,对象创建性能和分配给变量的性能。因此,实际速度差异可能更大一些。


答案 2

当我有一个字符串需要将单个字符连接到其末尾时,出于任何性能原因,我应该更喜欢s = .... + ']'而不是s = .... + “]”吗?

这里实际上有两个问题:

Q1:是否有性能差异?

答:这取决于...

  • 在某些情况下,可能是的,这取决于JVM和/或字节码编译器。如果字节码编译器生成对的调用,而不是,那么你会期望前者更快。但是 JIT 编译器可以将这些方法视为“intrinics”,并使用一个字符(文本)字符串优化对的调用。StringBuilder.append(char)StringBuilder.append(String)append(String)

    简而言之,您需要在平台上对此进行基准测试才能确定。

  • 在其他情况下,绝对没有区别。例如,这两个调用将被编译为相同的字节码序列,因为串联是常量表达式

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

    这由 JLS 保证。

Q2:您是否更喜欢一个版本而不是另一个版本。

答:

  • 从一般意义上讲,这可能是过早的优化。出于性能原因,如果您已在应用程序级别分析了代码并确定代码段对性能有可衡量的影响,则只应选择一种形式而不是另一种形式。

  • 如果已分析代码,请使用 Q1 的答案作为指南。

    如果值得尝试优化代码段,那么在优化后重新运行基准测试/分析,看看它是否有任何区别是至关重要的。你对什么是最快的直觉...以及您在互联网上的一些旧文章中读到的内容...可能是非常错误的。