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

我们必须一直为日志输出构建字符串等等。在JDK版本中,我们已经学习了何时使用StringBuffer(许多追加,线程安全)和StringBuilder(许多追加,非线程安全)。

使用String.format()的建议是什么?它是有效的,还是我们被迫坚持在性能很重要的一行程序中使用连接?

例如,丑陋的老式风格,

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

vs.整洁的新样式(字符串。格式,可能更慢),

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

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

174303 次浏览

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

现在,如果你不打算转换任何东西,那么要么依赖Java内置的+运算符转换为StringBuilder。或者显式地使用Java的StringBuilder

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

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

在您的示例中,性能可能没有太大不同,但还有其他问题需要考虑:即内存碎片。连接操作也在创建一个新字符串,即使它是临时的(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));

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

我写了一个小类来测试两者中哪个具有更好的性能,并且+优先于格式。以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

我只是修改了hhafez的测试,以包括StringBuilder。StringBuilder比String快33倍。格式使用jdk 1.6.0_10客户端XP。使用-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);
}
}

虽然这听起来可能很激烈,但我认为它只在极少数情况下相关,因为绝对数字非常低:100万个简单字符串对应4个s。格式调用是可以的-只要我使用他们的日志或类似的。

正如sjbotha在评论中指出的,StringBuilder测试是无效的,因为它缺少最后的.toString()

在我的机器上,从String.format(.)StringBuilder的正确加速因子是23(使用-server开关时为16)。

要展开/纠正上面的第一个答案,它不是翻译那个字符串。实际上,格式会有所帮助。< br > 字符串。format可以帮助您打印日期/时间(或数字格式等),其中存在本地化(l10n)差异(即,一些国家将打印04Feb2009,而其他国家将打印Feb042009)。< br > 在翻译中,您只是在谈论将任何外部化字符串(如错误消息等)移动到属性包中,以便使用ResourceBundle和MessageFormat为正确的语言使用正确的包
从以上来看,我认为在性能方面,String。格式vs.普通连接取决于你喜欢什么。如果您更喜欢对.format的调用而不是连接,那么无论如何都要这样做。< br > 毕竟,读代码的人要比写代码的人多得多。< / p >

我取了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次):
Approach      时间(毫秒)  已分配的内存(长)
' + ' operator ,,,, 747,,,,,,,,,, 320504,
String.format , 16484,,,,,, 373312,
StringBuilder , 769,,,,,,,,,, 57344,
< /强> < / p > 我们可以看到String '+'和StringBuilder在时间上实际上是相同的,但是StringBuilder在内存使用上要高效得多。 当我们在足够短的时间间隔内有许多日志调用(或任何其他涉及字符串的语句)时,这是非常重要的,这样垃圾收集器就不会清理'+'操作符导致的许多字符串实例

顺便说一句,在构造消息之前,不要忘记检查日志水平

结论:

  1. 我将继续使用StringBuilder。
  2. 我有的是时间,有的是生活。

你的旧样式会被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是没有区别的。

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

Java的字符串。格式如下:

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

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

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

我推测优化器将优化掉格式字符串处理。如果是这样,你就有了相同的摊销性能来手动展开你的String。格式转换为StringBuilder。

下面是修改后的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

后的时间

仅从日志的角度看另一个角度。

我在这个帖子上看到了很多关于登录的讨论,所以我想在回答中加入我的经验。也许有人会觉得有用。

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

实际上不需要concat/format,除非您想记录日志。假设我这样定义一个方法

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

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

尽管在这里使用StringBuilder而不是formatter会更好。我们的主要动机就是避免这些。

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

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

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

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

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

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

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

如果你有超过3个字符串,考虑使用StringBuilder,或者只使用String,这取决于你使用的编译器。