【问题标题】:Can I precompile the format string in String.format? (Or do any other thing to make formatting logs faster?)我可以在 String.format 中预编译格式字符串吗? (或者做任何其他事情来使格式化日志更快?)
【发布时间】:2015-06-27 04:19:19
【问题描述】:

众所周知that String.format() performance is terrible。我在我的(并且可能非常常见的)典型案例中看到了很大的可能改进。我多次打印相同的数据结构。让我们想象一下像“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;

对于 1e6 迭代,预格式化将结果提高了 ~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 32bit):

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 

[EDIT2]

正如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();
}

上面 [EDIT] 部分给出的 MessageFormat 代码。我确实得到了以下结果,循环了 100 万次:

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

所以到目前为止,MessageFormatter 是最好的答案,但我的简单示例仍然快一点……所以任何现成的更快的库提案?

【问题讨论】:

  • 循环迭代多少次?
  • String#format() 主要用于调试和临时输出,一般不需要很快。
  • 如果你需要性能,你应该使用StringBuilder
  • @SashaSalauyou 好吧,我会说反过来: String#format() 表现不佳;因此它不应该在生产代码中广泛使用。但这是一个必须知道的事情;例如,该方法的 Javadoc 肯定没有提到它。我认为它完全不同:“基础”方法,可以被调用数十亿次,应该尽可能快。意思是:你在我眼里混淆了因果。
  • @Jägermeister 好吧...看了Formatter 源代码后,我同意。我对在那里使用正则表达式而不是流解析感到失望。

标签: java string performance optimization string-formatting


【解决方案1】:

听起来你想要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));

【讨论】:

  • 我不知道 - 那里可能有基准。但是,它满足了预编译消息的要求。就个人而言,我会选择 StringBuilder。
  • 预编译没有“要求”。这只是一种提高性能的尝试。
  • FWIW,MessageFormat 确实对模式进行了一些编译,因此至少有可能会相当快。
  • 有一个旧的(2009 年旧!)比较here,这表明性能很差。这并不能说明现在的情况,但在过去的某个时候,它完全糟透了。
  • 也在这里:stackoverflow.com/questions/15358090/… 但我会忽略这两种比较,因为它们只使用一次该模式。预编译没有任何好处。
【解决方案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 重现任何显着的性能改进,而且它还给出了基于当前语言环境的略有不同的结果(这可能既是优点也是缺点)。
  • 谢谢回答。迄今为止最快的解决方案。但我的测量(见我编辑的问题)显示只比 MessageFormat 快 2.4 倍。不像你或 SLF4J 宣布 5 次。我多次尝试循环执行 1M。
  • @VitBernatik 也许是因为我可以重现您对 MessageFormat 的改进。这就是微基准的问题——它们都有缺陷,但对于每个人、每台机器,它们都有不同的缺陷。无论哪种方式,我都为 slf4j 提出了一个错误,以获得更好的 API,并为 Guava 发布其内部实现。我希望这会有所改善,这项任务太常见了,无法一直手动编写。
  • 是的,slf4j 可能还有改进的空间。他们可以允许像 MessageFormat 那样对其进行预解析。然后我们可以对普通日志有更快的速度。我还读到 log4j v2 是最快的。你认为它只能用于消息格式化吗?因为我确实有自己的日志记录机制(例如支持标签,并通过标签过滤......)
  • @VitBernatik(在上一条消息中,我的意思是写“无法重现”)...无论哪种方式,您都可以自己尝试 log4j2 的实现:ParameterizedMessage。同样,不幸的是它不会缓存/编译消息格式,我希望看到一些实现这样做。如果你没有比我快,我会在星期一添加 ParameterizedMessage 的测量值。
【解决方案3】:

我说我会送货,就在这里。我的预编译字符串格式化(工作概念验证)库: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/3。请注意,此时,您必须对大量字符串进行格式化才能获得显着差异。

【讨论】:

  • 在进行优化时,您可能需要提供appendTo 来代替(或补充)format。在循环中,您可能希望连接所有字符串而不生成它们(即format.appendTo(sb, i, i, i) 而不是sb.append(format.format(i, i, i))。中间字符串可能比装箱和可变参数更昂贵。
  • @maaartinus 感谢您的想法!在相关的说明中,我今天想到的一件事是对Collection 类的特殊处理——也在内部为它们添加了一个自定义的appendTo() 方法。不过,你的想法让我忘记了,我很乐意实现它。 (不过,老实说,这样一个库的实际使用率可能接近于零。尽管如此,这是一个有趣的问题,我不敢相信我没有找到任何现有的实现。)
  • 问题是 String.format 的存在,这是从 C 语言重制的该死的愚蠢和该死的慢。不管它有多糟糕,它涵盖了许多用例,这就是为什么没人愿意做某事的原因更好(i18n、日志记录和 Guava 的前提条件都只处理特殊情况)。一旦我有了编写一个可定制的格式化程序的想法,您可以在其中插入自己的类和格式化字符串的处理程序,然后像 format("time=[t%HH:mm:dd] wtf=[t]", System,currentTimeMillis(), someException) 一样调用它并获取它包括堆栈跟踪。它比String.format 快得多。
  • String.format 的其他问题是它可以抛出,如果仅用于日志记录,那就太糟糕了。它不能处理byte[],它不能使用_ 作为千位分隔符(Java 源兼容的输出有时很好)。这一切都是可行的(并且部分完成了),但现实世界的使用令人沮丧。关心chat?我只是在看你的代码。
  • @maaartinus 在工作中,在移动设备上。我很乐意稍后加入,cca 在 4 小时后,在 19.30 UTC。
猜你喜欢
  • 1970-01-01
  • 2011-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多