如果性能很重要,我应该使用Java的String.format()吗?

时间:2009-02-04 22:09:29

标签: java string performance string-formatting micro-optimization

我们必须始终为日志输出构建字符串,依此类推。在JDK版本中,我们学习了何时使用StringBuffer(许多追加,线程安全)和StringBuilder(许多追加,非线程安全)。

使用String.format()的建议是什么?它是否有效,或者我们是否被迫坚持连接性能很重要的单线?

e.g。丑陋的旧式,

String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";

VS。整洁的新风格(String.format,可能更慢),

String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);

注意:我的具体用例是我的代码中数百个“单线”日志字符串。它们不涉及循环,因此StringBuilder太重了。我特别感谢String.format()

13 个答案:

答案 0 :(得分:232)

我使用了hhafez代码并添加了内存测试

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

我为每个方法单独运行它,'+'运算符,String.format和StringBuilder(调用toString()),因此使用的内存不会受到其他方法的影响。 我添加了更多连接,使字符串为“Blah”+ i +“Blah”+ i +“Blah”+ i +“Blah”。

结果如下(每次平均5次运行):
接近时间(ms)分配的内存(长)
'+'运算符747 320,504
16484的String.format
373312 StringBuilder的769 57,344

我们可以看到String'+'和StringBuilder在时间上几乎完全相同,但StringBuilder在内存使用方面更有效。 当我们在足够短的时间间隔内有许多日志调用(或任何其他涉及字符串的语句)时,这非常重要,因此垃圾收集器将无法清除由“+”运算符产生的许多字符串实例。

请注意,BTW,在构建消息之前不要忘记检查记录级别

结论:

  1. 我将继续使用StringBuilder。
  2. 我有太多时间或太少的生活。

答案 1 :(得分:115)

我写了一个小类来测试哪个具有更好的性能,并且+优先于格式。 5到6倍。 亲自尝试

import java.io.*;
import java.util.Date;

public class StringTest{

    public static void main( String[] args ){
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;

    for( i = 0; i< 100000; i++){
        String s = "Blah" + i + "Blah";
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<100000; i++){
        String s = String.format("Blah %d Blah", i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    }
}

对不同的N运行以上操作表明两者都表现出线性,但String.format慢了5-30倍。

原因是在当前实现中String.format首先使用正则表达式解析输入,然后填充参数。另一方面,与plus的串联由javac(而不是JIT)优化并直接使用StringBuilder.append

Runtime comparison

答案 2 :(得分:23)

此处提供的所有基准都有一些flaws,因此结果不可靠。

我很惊讶没有人使用JMH进行基准测试,所以我做了。

结果:

Benchmark             Mode  Cnt     Score     Error  Units
MyBenchmark.testOld  thrpt   20  9645.834 ± 238.165  ops/s  // using +
MyBenchmark.testNew  thrpt   20   429.898 ±  10.551  ops/s  // using String.format

单位是每秒的操作,越多越好。 Benchmark source code。使用OpenJDK IcedTea 2.5.4 Java虚拟机。

所以,旧式(使用+)要快得多。

答案 3 :(得分:20)

你的丑陋风格由JAVAC 1.6自动编译为:

StringBuilder sb = new StringBuilder("What do you get if you multiply ");
sb.append(varSix);
sb.append(" by ");
sb.append(varNine);
sb.append("?");
String s =  sb.toString();

因此,使用StringBuilder完全没有区别。

String.format是更重量级的,因为它创建了一个新的Formatter,解析了你的输入格式字符串,创建了一个StringBuilder,将所有内容附加到它并调用toString()。

答案 4 :(得分:12)

Java的String.format的工作原理如下:

  1. 它解析格式字符串,爆炸成格式块列表
  2. 它迭代格式块,渲染成StringBuilder,它基本上是一个通过复制到新数组来根据需要调整自身大小的数组。这是必要的,因为我们还不知道分配最终字符串的大小
  3. StringBuilder.toString()将其内部缓冲区复制到新的String
  4. 如果此数据的最终目的地是流(例如渲染网页或写入文件),则可以将格式块直接汇编到流中:

    new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");
    

    我推测优化器会优化格式字符串处理。如果是这样,您将获得相同的amortized性能,以便将String.format手动展开到StringBuilder中。

答案 5 :(得分:8)

要扩展/纠正上面的第一个答案,实际上并不是String.format会提供帮助的翻译。
String.format将帮助您的是当您打印日期/时间(或数字格式等),其中存在本地化(l10n)差异(即,一些国家将打印04Feb2009而其他国家将打印Feb042009)。
通过翻译,您只是谈论将任何可外部化的字符串(如错误消息和什么不是)移动到属性包中,以便您可以使用正确的语言包使用ResourceBundle和MessageFormat。
综上所述,我会说性能方面,String.format与简单连接归结为你喜欢的。如果您更喜欢查看.format对连接的调用,那么请务必使用它。
毕竟,代码的读取次数远远超过它的编写。

答案 6 :(得分:6)

在您的示例中,性能probalby并没有太大的不同,但还有其他问题需要考虑:即内存碎片。甚至连接操作也会创建一个新字符串,即使它是临时的(GC需要时间,而且工作更多)。 String.format()只是更具可读性,它涉及更少的碎片。

另外,如果您经常使用特定格式,请不要忘记您可以直接使用Formatter()类(所有String.format()都会实例化一个使用Formatter实例。)

另外,您应该注意的其他事项:小心使用substring()。例如:

String getSmallString() {
  String largeString = // load from file; say 2M in size
  return largeString.substring(100, 300);
}

那个大字符串仍在内存中,因为这只是Java子字符串的工作方式。更好的版本是:

  return new String(largeString.substring(100, 300));

  return String.format("%s", largeString.substring(100, 300));

如果你同时做其他事情,第二种形式可能会更有用。

答案 7 :(得分:5)

通常你应该使用String.Format,因为它相对较快而且它支持全球化(假设你实际上是在尝试编写用户读取的东西)。如果你试图翻译一个字符串而不是每个语句3个或更多,那么它也更容易全球化(特别是对于语法结构截然不同的语言)。

现在,如果您从未计划翻译任何内容,那么要么依赖Java内置的+运算符转换为StringBuilder。或者明确使用Java的StringBuilder

答案 8 :(得分:3)

仅从日志记录的角度来看另一个视角。

我看到很多关于登录这个帖子的讨论,所以想到在回答中添加我的经验。可能有人会发现它很有用。

我想使用格式化程序进行日志记录的动机来自于避免字符串连接。基本上,如果你不打算记录它,你不希望有字符串concat的开销。

除非你想记录,否则你真的不需要连接/格式化。让我们说如果我定义一个像这样的方法

public void logDebug(String... args, Throwable t) {
    if(debugOn) {
       // call concat methods for all args
       //log the final debug message
    }
}

在这种方法中,如果调试消息和debugOn = false

,则根本不会真正调用cancat / formatter

虽然在这里使用StringBuilder而不是formatter仍然会更好。主要动机是避免任何这种情况。

同时我不喜欢为每个日志语句添加“if”块,因为

  • 影响可读性
  • 减少单元测试的覆盖范围 - 当您想要确保每一行都经过测试时,这会让您感到困惑。

因此,我更喜欢使用上述方法创建一个日志记录实用程序类,并在任何地方使用它,而不必担心性能损失以及与之相关的任何其他问题。

答案 9 :(得分:2)

我刚修改了hhafez的测试以包含StringBuilder。使用XP上的jdk 1.6.0_10客户端,StringBuilder比String.format快33倍。使用-server开关可将系数降低到20.

public class StringTest {

   public static void main( String[] args ) {
      test();
      test();
   }

   private static void test() {
      int i = 0;
      long prev_time = System.currentTimeMillis();
      long time;

      for ( i = 0; i < 1000000; i++ ) {
         String s = "Blah" + i + "Blah";
      }
      time = System.currentTimeMillis() - prev_time;

      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         String s = String.format("Blah %d Blah", i);
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         new StringBuilder("Blah").append(i).append("Blah");
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);
   }
}

虽然这可能听起来很激烈,但我认为它只在极少数情况下才有用,因为绝对数字非常低:1百万个简单的String.format调用4秒就好了 - 只要我用它们就可以了记录等。

更新:正如sjbotha在评论中所指出的,StringBuilder测试无效,因为它缺少最终的.toString()

我的计算机上String.format(.)StringBuilder的正确加速因子为23(-server开关为16)。

答案 10 :(得分:1)

这是hhafez条目的修改版本。它包括字符串构建器选项。

public class BLA
{
public static final String BLAH = "Blah ";
public static final String BLAH2 = " Blah";
public static final String BLAH3 = "Blah %d Blah";


public static void main(String[] args) {
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;
    int numLoops = 1000000;

    for( i = 0; i< numLoops; i++){
        String s = BLAH + i + BLAH2;
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

循环391之后的时间 循环4163之后的时间 循环227之后的时间

答案 11 :(得分:0)

这个问题的答案在很大程度上取决于您的特定Java编译器如何优化它生成的字节码。字符串是不可变的,理论上,每个“+”操作都可以创建一个新的字符串。但是,您的编译器几乎肯定会优化构建长字符串的临时步骤。上面的两行代码完全可能生成完全相同的字节码。

唯一真正知道的方法是在当前环境中迭代地测试代码。编写一个QD应用程序,以迭代方式连接字符串,并查看它们如何相互超时。

答案 12 :(得分:0)

考虑在连接中使用"hello".concat( "world!" )来处理少量字符串。性能可能比其他方法更好。

如果您有超过3个字符串,请考虑使用StringBuilder或String,具体取决于您使用的编译器。