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


215

我们必须一直构建String以便进行日志输出等等。在JDK版本中,我们了解了何时使用StringBuffer(很多追加,线程安全)和StringBuilder(很多追加,非线程安全)。

有什么使用建议String.format()?是高效的,还是在性能很重要的情况下,我们被迫坚持采用单线连接?

例如丑陋的旧风格,

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

与整洁的新样式(String.format,可能会更慢)相比,

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

注意:我的特定用例是整个代码中数百个“单行”日志字符串。它们不涉及循环,所以StringBuilder也太笨重。我String.format()特别感兴趣。


28
你为什么不测试呢?
Ed S.

1
如果要生成此输出,那么我认为它必须是人类可读的,因为人类可以阅读它。假设每秒最多10行。我认为您会发现采用哪种方法真的无关紧要,如果从概念上讲速度较慢,则用户可能会喜欢它。;)因此,StringBuilder在大多数情况下都不是重量级的。
彼得·劳瑞

9
@Peter,不,这绝对不是人类实时阅读的!当出现问题时,它可以帮助分析。日志输出通常为每秒数千行,因此需要提高效率。
航空

5
如果您每秒产生数千行,我建议1)使用较短的文本,甚至不使用纯CSV或二进制文件之类的文本2)完全不使用String,可以将数据写入ByteBuffer而不创建任何对象(如文本或二进制)3)将数据写入磁盘或套接字的后台。您应该能够每秒维持约一百万条线路。(基本上是磁盘子系统所允许的)。您可以达到10倍的突发次数。
彼得·劳瑞

7
这与一般情况无关,但是对于特定的日志记录,LogBack(由Log4j的原始作者编写)具有一种参数化日志记录形式,可以解决此确切问题-logback.qos.ch/manual/architecture.html#ParametrizedLogging
马特·帕塞尔

Answers:


122

我编写了一个小类进行测试,该类具有两者的更好性能,并且+领先于格式。以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直接使用。

运行时比较


12
此测试有一个缺陷,即它不能完全代表所有字符串格式。通常要包含什么逻辑,以及将特定值格式化为字符串的逻辑。任何真实的测试都应该考虑真实的场景。
Orion Adrian

9
SO上还有另一个关于StringBuffer的问题,在Java +的最新版本中,如果可能,将
JavaString

25
这看起来很像一种微基准,它将以一种非常无用的方式进行优化。
David H. Clements 2012年

20
另一个执行不佳的微基准测试。两种方法如何按数量级缩放。如何使用100、1000、10000、1000000操作。如果您仅在未在隔离核心上运行的应用程序上运行一个数量级的测试,则该测试将被执行。有没有办法告诉多少的差异可以为由于上下文切换,后台进程等“副作用”被注销
埃文鲽

8
此外,你永远不要离开主JIT不能一命呜呼
扬Zyka

241

我使用了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次):
接近时间(毫秒)分配的内存(长)
'+'运算符747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

我们可以看到String'+'和StringBuilder在时间上实际上是相同的,但是StringBuilder在内存使用方面效率更高。当我们在足够短的时间间隔内进行许多日志调用(或任何其他其他涉及字符串的语句)时,这非常重要,这样垃圾收集器将无法清除由'+'运算符引起的许多字符串实例。

还有一个注意事项,顺便说一句,不要忘记在构造消息之前检查日志记录级别

结论:

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

8
“不要忘记在构造消息之前检查日志记录级别”,这是一个很好的建议,至少应该对调试消息执行此操作,因为可能有很多消息,并且不应在生产中启用它们。
stivlo 2011年

39
不,这是不对的。抱歉,直言不讳,但它吸引了许多赞誉,令人震惊。使用+运算符可编译为等效StringBuilder代码。这样的微基准并不是衡量性能的好方法-为什么不使用jvisualvm,这是有原因的。String.format() 比较慢,但是由于要分析格式字符串而不是任何对象分配而需要时间。在您确定需要日志记录工件之前,将它们推迟创建是一个很好的建议,但是,如果这会对性能产生影响,那就错了。
CurtainDog 2013年

1
@CurtainDog,您的评论是对一个已有四年历史的帖子的,您可以指向文档或创建一个单独的答案来解决这一差异吗?
kurtzbot 2014年

1
支持@CurtainDog评论的参考:stackoverflow.com/a/1532499/2872712。也就是说,除非循环执行,否则+是首选。

And a note, BTW, don't forget to check the logging level before constructing the message.不是很好的建议。假设我们正在java.util.logging.*具体讨论,检查日志记录级别就是在您进行高级处理时,这会对程序造成不利影响,而在程序未将日志记录打开到适当级别时,您将不希望这样做。字符串格式化不是那种处理方式。格式化是java.util.logging框架的一部分,记录器本身会在调用格式化程序之前检查日志记录级别。
searchengine27

30

这里介绍的所有基准测试都有一些缺陷,因此结果不可靠。

我很惊讶没有人使用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

单位是每秒的操作数,越多越好。基准源代码。使用了OpenJDK IcedTea 2.5.4 Java虚拟机。

因此,旧样式(使用+)要快得多。


5
如果您注释哪个是“ +”和哪个是“ format”,则这将更容易解释。
AjahnCharles

21

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()。


就可读性而言,您发布的代码比String.format麻烦得多……(“如果将%d乘以%d会得到什么?”,varSix,varNine);
黄昏者

12
+StringBuilder确实没有区别。不幸的是,在该线程的其他答案中存在很多错误信息。我几乎想把问题改成how should I not be measuring performance
CurtainDog 2013年

12

Java的String.format的工作方式如下:

  1. 它解析格式字符串,爆炸成格式块列表
  2. 它迭代格式块,渲染成StringBuilder,它基本上是一个数组,可以通过复制到新数组来根据需要调整自身大小。这是必要的,因为我们尚不知道分配最终String的大小
  3. StringBuilder.toString()将其内部缓冲区复制到新的String中

如果此数据的最终目的地是流(例如,渲染网页或写入文件),则可以将格式块直接组装到流中:

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

我推测优化器将优化格式字符串处理。如果是这样,您将获得同等的摊销性能,可以手动将String.format展开为StringBuilder。


5
我认为您对格式字符串处理优化的猜测是不正确的。在一些使用Java 7进行的实际测试中,我发现String.format在内部循环中使用(运行数百万次)导致我的执行时间花费了10%以上java.util.Formatter.parse(String)。这似乎表明,在内部循环中,应避免调用Formatter.format或调用它的任何方法,包括PrintStream.format(Java标准库IMO中的一个缺陷,尤其是因为无法缓存已解析的格式字符串)。
安迪·麦金莱

8

为了扩展/更正上面的第一个答案,实际上不是String.format会帮助您进行翻译。
String.format将帮助您的是当您打印日期/时间(或数字格式等)时,本地化(l10n)有所不同(即,某些国家/地区将打印04Feb2009,而其他国家/地区将打印Feb042009)。
对于翻译,您只是在谈论将任何可外部化的字符串(例如错误消息和诸如此类的东西)移到属性束中,以便可以使用ResourceBundle和MessageFormat将正确的束用于正确的语言。

综上所述,我认为在性能方面,String.format与普通串联取决于您的喜好。如果您希望查看对.format的调用而不是串联,那么请务必这样做。
毕竟,对代码的读取远比其编写的要多。


1
我会说从性能角度来看,String.format与普通串联可以归结为您希望的结果,我认为这是不正确的。从性能角度来看,连接要好得多。有关更多详细信息,请查看我的答案。
亚当·斯泰尔马什奇克

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

如果您同时执行其他操作,则第二种形式可能更有用。


8
值得指出的“相关问题”实际上是C#,因此不适用。
航空

您使用了哪个工具来测量内存碎片,碎片甚至会影响ram的速度?
kritzikratzi

值得指出的是,substring方法是从Java 7 +更改的。现在,它应该返回仅包含子字符串字符的新String表示形式。这意味着,没有必要回电话字符串::新
若昂·雷贝洛

5

通常,您应该使用String.Format,因为它相对较快并且支持全球化(假设您实际上是在尝试编写用户可以读取的内容)。如果您要翻译一个字符串而不是每个语句3个或更多的字符串,这也使全球化变得更加容易(尤其是对于语法结构截然不同的语言)。

现在,如果您从不打算翻译任何内容,则可以依靠Java内置的+运算符到的转换StringBuilder。或StringBuilder显式使用Java 。


3

从Logging角度来看的另一种观点。

我看到了很多与登录此线程有关的讨论,因此想在回答中增加我的经验。可能有人会发现它有用。

我猜想使用格式化程序进行记录的动机来自避免字符串串联。基本上,如果您不打算记录字符串,则不需要字符串concat的开销。

除非您要登录,否则您实际上不需要concat / format。可以说是否定义了这样的方法

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

在这种方法中,如果cancat / formatter是一条调试消息且debugOn = false,则根本不会真正调用它

尽管在这里使用StringBuilder代替formatter还是更好。主要动机是避免任何这种情况。

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

  • 它影响可读性
  • 减少我的单元测试的覆盖范围-当您要确保每行都经过测试时,这很令人困惑。

因此,我更喜欢使用上面的方法创建一个日志记录实用工具类,并在各处使用它,而不必担心性能下降以及与此有关的任何其他问题。


您能否利用现有的库(如slf4j-api)来声称通过其参数化的日志记录功能来解决该用例?slf4j.org/faq.html#logging_performance
ammianus

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

尽管这听起来可能很激烈,但我认为它仅在极少数情况下才有意义,因为绝对数字非常低:一百万个简单String.format调用的4 s没问题-只要我将它们用于日志记录或喜欢。

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

我的机器上从String.format(.)到的正确加速因子StringBuilder是23(带-server开关的16 )。


1
您的测试无效,因为它没有考虑仅通过循环而消耗的时间。您应该包括该值,并至少从所有其他结果中减去它(是的,这可能是一个很大的百分比)。
cletus

我这样做了,for循环需要0毫秒。但是,即使确实需要时间,这也只会增加因素。
the.duckman's

3
StringBuilder测试无效,因为它最后没有调用toString()来实际提供可以使用的String。我添加了它,结果是StringBuilder花费的时间与+差不多。我敢肯定,随着您增加附件的数量,它最终将变得更便宜。
Sarel Botha

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之后的时间


0

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

唯一真正了解的方法是在当前环境中迭代测试代码。编写一个QD应用程序,以迭代方式将字符串连接起来,并查看它们如何彼此超时。


1
第二个示例的字节码肯定调用String.format,但如果进行简单的串联操作,我会感到恐惧。为什么编译器会使用格式字符串,然后必须对其进行解析?
乔恩·斯基特

我在应该说“二进制代码”的地方使用了“字节码”。当全部归结为jmps和movs时,它可能完全是相同的代码。
是的-那个杰克。

0

考虑"hello".concat( "world!" )在串联中使用少量字符串。它的性能可能比其他方法更好。

如果您有3个以上的字符串,则可以考虑使用StringBuilder或仅使用String,具体取决于您使用的编译器。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.