我能否在String.format中预编译格式字符串?(或者做其他任何事情来加快日志格式化的速度?)

23

众所周知,String.format()的性能很差。在我(也可能是普遍情况)打印相同数据结构多次的情况下,我看到了可能有很大的改进空间。假设这个结构像“x:%d y:%d z:%d”。我预计 String.format() 的主要问题在于它必须始终解析格式化字符串。我的问题是:是否有一些现成的类可以允许只读取格式化字符串一次,然后在填充变量参数时快速提供字符串?使用应该如下:

PreString ps = new PreString("x:%d y:%d z:%d");
String s;
for(int i=0;i<1000;i++){
    s = ps.format(i,i,i); 
}

我知道这是可能的——以下是我的快速且不太规范的示例,它可以做到我所说的,并且在我的机器上大约快了10倍:

public interface myPrintable{
    boolean isConst();
    String prn(Object o);
    String prn();
}

public class MyPrnStr implements myPrintable{
    String s;
    public MyPrnStr(String s){this.s =s;}
    @Override public boolean isConst() { return true; }
    @Override public String prn(Object o) { return s; }
    @Override public String prn() { return s; }
}

public class MyPrnInt implements myPrintable{
    public MyPrnInt(){}
    @Override  public boolean isConst() { return false; }
    @Override  public String prn(Object o) { return String.valueOf((Integer)o);  }
    @Override  public String prn() { return "NumMissing";   }
}

public class FastFormat{
    myPrintable[]      obj    = new myPrintable[100];
    int                objIdx = 0;
    StringBuilder      sb     = new StringBuilder();

    public FastFormat() {}

    public void addObject(myPrintable o) {  obj[objIdx++] = o;   }

    public String format(Object... par) {
        sb.setLength(0);
        int parIdx = 0;
        for (int i = 0; i < objIdx; i++) {
            if(obj[i].isConst()) sb.append(obj[i].prn());
            else                 sb.append(obj[i].prn(par[parIdx++]));
        }
        return sb.toString();
    }
}

它的使用方法如下:

FastFormat ff = new FastFormat();
ff.addObject(new MyPrnStr("x:"));
ff.addObject(new MyPrnInt());
ff.addObject(new MyPrnStr(" y:"));
ff.addObject(new MyPrnInt());
ff.addObject(new MyPrnStr(" z:"));
ff.addObject(new MyPrnInt());
for (int i = 0; i < rpt; i++) {
    s = ff.format(i,i,i);
}

当我进行比较时

long beg = System.nanoTime();
for (int i = 0; i < rpt; i++) {
    s = String.format("x:%d y:%d z:%d", i, i, i);
}
long diff = System.nanoTime() - beg;

对于100万次迭代,预格式化可以将结果提高约10倍:

time [ns]: String.format()     (+90,73%)  3 458 270 585 
time [ns]: FastFormat.format() (+09,27%)    353 431 686 

[编辑]

正如Steve Chaloner所回复的,有一个MessageFormat可以满足我的需求。因此我尝试了以下代码:

MessageFormat mf = new MessageFormat("x:{0,number,integer} y:{0,number,integer} z:{0,number,integer}");
Object[] uo = new Object[3];
for (int i = 0; i < rpt; i++) {
    uo[0]=uo[1]=uo[2] = i;
    s = mf.format(uo);
}

仅快了2倍,而不是我所希望的10倍。再次查看1M迭代(JRE 1.8.0_25-b18 32位)的测量结果:

time [s]: String.format()     (+63,18%)  3.359 146 913 
time [s]: FastFormat.format() (+05,99%)  0.318 569 218 
time [s]: MessageFormat       (+30,83%)  1.639 255 061 

[编辑2]

Slanec所回答,有一个org.slf4j.helpers.MessageFormatter。 (我尝试了库版本slf4j-1.7.12

我试图比较代码:

Object[] uo2 = new Object[3];
beg = System.nanoTime();
for(long i=rpt;i>0;i--){
    uo2[0]=uo2[1]=uo2[2] = i;
    s = MessageFormatter.arrayFormat("x: {} y: {} z: {}",uo2).getMessage();
}

使用上面[编辑]部分给出的MessageFormat代码,我对其进行了100万次循环运算,结果如下:

Time MessageFormatter [s]: 1.099 880 912
Time MessageFormat    [s]: 2.631 521 135
speed up : 2.393 times

因此,目前为止,MessageFormatter是最好的答案,但我的简单示例仍然快了一点...那么有没有现成的更快的库建议呢?


3
循环迭代几次? - GhostCat
5
String#format() 主要用于调试和临时输出,通常不需要非常快速。 - Alex Salauyou
4
我认为相反的是正确的:String#format()的性能很差,因此在生产代码中不应大量使用。但这是必须要知道的;例如,该方法的Javadoc肯定没有提到这一点。在我看来,"基础"方法可能被调用数十亿次,因此应尽可能快。也就是说,你混淆了因果关系。 - GhostCat
1
@Jägermeister 嗯...在查看了Formatter的源代码后,我同意。我对在那里使用正则表达式而不是流解析感到失望。 - Alex Salauyou
1
这里的关键点在于灵活性。当然,创建一个快速将3个默认格式化的“int”值附加到字符串中的内容是微不足道的。但是,如果您想将其扩展为支持科学记数法、右对齐的 BigDecimal 值和中文 Locale,那么您最终会实现所有细节,而这些细节现在已由 Formatter 类覆盖。如果您更清晰地描述了所需的应用场景,可能会想出一种优化的解决方案。 - Marco13
显示剩余11条评论
3个回答

9

听起来你需要使用MessageFormat

根据文档:

以下示例创建一个可重复使用的MessageFormat实例:

 int fileCount = 1273;
 String diskName = "MyDisk";
 Object[] testArgs = {new Long(fileCount), diskName};

 MessageFormat form = new MessageFormat(
     "The disk \"{1}\" contains {0} file(s).");
 System.out.println(form.format(testArgs));

2
没有“必须”的预编译要求,这只是为了提高性能的尝试。 - Thilo
2
就此而言,MessageFormat确实对模式进行了一些编译,因此至少有可能使其相当快速。 - Thilo
1
这里有一个旧的(2009年的!)比较链接,表明性能非常糟糕。虽然这并不代表现在的情况,但在过去的某个时候它确实很差。 - Steve Chaloner
1
同样的内容也可以在这里找到:https://dev59.com/jm_Xa4cB1Zd3GeqP2Iht 但是我会忽略这两个比较,因为它们只使用一次模式。预编译并没有任何好处。 - Thilo
谢谢回复!我尝试了一下,它是我想要的,并且速度也更快!不幸的是,它只比预期的快了2倍,而不是10倍。请参见我的编辑后的问题。因此,我将继续保持这个问题开放 - 看看是否有人能够向我展示如何使它变得更快一点。 - Vit Bernatik
显示剩余2条评论

2

如果你想要快速实现,需要在JDK之外寻找。你可能已经使用slf4j进行日志记录,因此让我们看看它的MessageFormatter

MessageFormatter.arrayFormat("x:{} y:{} z:{}", new Object[] {i, i, i}).getMessage();

在我的机器上(通过简单而有缺陷的微型基准测试),它比你的FastFormat类慢大约1/6,但比String::formatMessageFormat快5-10倍。

顺便说一句,在Java 8上,我并不能通过使用MessageFormat来获得任何显著的性能提升。而且,它还会根据当前的区域设置略有不同的结果(这可能既是一个优点也是一个缺点)。 - Petr Janeček
谢谢您的回答。到目前为止,这是最快的解决方案。但我的测量数据(请参见我的编辑后的问题)仅显示比MessageFormat快2.4倍。而不是您或SLF4J宣传的5倍。我尝试了多次执行100万次循环。 - Vit Bernatik
1
@VitBernatik 也许是因为我能够重现您的MessageFormat改进。这就是微基准测试的问题-它们都有缺陷,但对于每个人和每台机器,它们的缺陷都不同。无论如何,我为slf4j提出了一个错误,以获得更好的API,并要求Guava发布其内部实现。我希望这将改善,因为手动编写此任务太常见了。 - Petr Janeček
是的,slf4j 可能仍有改进空间。它们可以像 MessageFormat 一样允许预解析。然后我们可以在常见日志中获得更快的速度。此外,我读到 log4j v2 是最快的。您认为它只能用于消息格式化吗?因为我有自己的日志记录机制(例如支持标签和按标签过滤...)。 - Vit Bernatik
1
@VitBernatik (在上一条消息中,我想写“无法重现”)......无论如何,您可以尝试log4j2的实现:[ParameterizedMessage](http://logging.apache.org/log4j/2.x/log4j-api/apidocs/org/apache/logging/log4j/message/ParameterizedMessage.html)。同样,它不幸地没有缓存/编译消息格式,我希望看到一些实现能够做到这一点。如果您比我快,我将在星期一添加`ParameterizedMessage`的测量值。 - Petr Janeček
显示剩余4条评论

1
我说过我会兑现承诺,这就是它。我的预编译字符串格式化(可工作的概念验证)库:https://gitlab.com/janecekpetr/string-format 使用方法:
StringFormat.format("x:{} y:{} z:{}", i, i, i)

我得到的数字与slf4j和log4j2非常相似。

然而,当使用时

CompiledStringFormat format = StringFormat.compile("x:{} y:{} z:{}");

// and then, in the loop
format.format(i, i, i)

我得到的数字比你的 FastFormat 大约好了三分之一。请注意,此时必须格式化大量字符串才能获得显著差异。

1
当进行优化时,您可能希望提供appendTo而不是(或除了)format。在循环中,您可能希望连接所有字符串而不生成它们(即使用format.appendTo(sb, i, i, i)而不是sb.append(format.format(i, i, i)))。中间字符串的成本可能比装箱和可变参数更高。 - maaartinus
@maaartinus 谢谢你的想法!在相关的注释中,我今天想到的一件事是对Collection类进行特殊处理 - 在内部为它们添加自定义的appendTo()方法。不过你的想法我忘了,我很乐意实现它。 (老实说,这样的库可能在现实世界中几乎没有任何用途。尽管如此,这是一个有趣的问题,我无法相信我没有找到任何现有的实现。) - Petr Janeček
1
问题在于存在String.format,它是从C语言中愚蠢而又缓慢的重制品。无论它有多糟糕,它都涵盖了许多用例,这就是为什么没有人关心做得更好(i18n、日志记录和Guava的前置条件都只处理特殊情况)。曾经我有一个想法,写一个可定制的格式化程序,在其中可以插入自己类的处理程序和格式化字符串,然后像这样调用它:format("time=[t%HH:mm:dd] wtf=[t]", System.currentTimeMillis(), someException),并包含堆栈跟踪。它比String.format快得多。 - maaartinus
String.format 的另一个问题是,如果仅用于日志记录,则可能会抛出异常,这太糟糕了。它无法处理 byte[],也无法使用 _ 作为千位分隔符(Java 源兼容输出有时很好)。所有这些都是可行的(部分已完成),但实际应用场景并不鼓舞人心。愿意聊天吗?我只是在看你的代码。 - maaartinus
@maaartinus 在工作中,使用手机。我将在4小时后的UTC时间19:30加入,请稍后。 - Petr Janeček
显示剩余6条评论

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