性能问题:与String.Format比较

不久之后Jon Skeet的一篇文章在构建CompiledFormatter类的过程中构建了这个想法,用于在循环中使用而不是String.Format()

我们的想法是,对String.Format()的调用部分用于解析格式字符串是开销。 我们应该能够通过将代码移到循环之外来提高性能。 当然,技巧是新代码必须与String.Format()行为完全匹配。

本周我终于做到了。 我实际上使用了Microsoft提供的.Net框架源来直接调整它们的解析器(事实certificate, String.Format()实际上将工作String.Format()StringBuilder.AppendFormat() )。 我提出的代码是有效的,因为我的结果在我的(公认有限的)测试数据中是准确的。

不幸的是,我还有一个问题:表现。 在我的初始测试中,我的代码的性能与普通的String.Format()的性能非常接近。 根本没有任何改进:它甚至一直慢几毫秒。 至少它仍然处于相同的顺序(即:缓慢的量不会增加;即使测试集增长,它仍然在几毫秒内),但我希望有更好的东西。

StringBuilder.Append()的内部调用可能是实际推动性能的因素,但我想看看这里聪明的人是否可以改进任何东西。

以下是相关部分:

 private class FormatItem { public int index; //index of item in the argument list. -1 means it's a literal from the original format string public char[] value; //literal data from original format string public string format; //simple format to use with supplied argument (ie: {0:X} for Hex // for fixed-width format (examples below) public int width; // {0,7} means it should be at least 7 characters public bool justify; // {0,-7} would use opposite alignment } //this data is all populated by the constructor private List parts = new List(); private int baseSize = 0; private string format; private IFormatProvider formatProvider = null; private ICustomFormatter customFormatter = null; // the code in here very closely matches the code in the String.Format/StringBuilder.AppendFormat methods. // Could it be faster? public String Format(params Object[] args) { if (format == null || args == null) throw new ArgumentNullException((format == null) ? "format" : "args"); var sb = new StringBuilder(baseSize); foreach (FormatItem fi in parts) { if (fi.index = args.Length) throw new FormatException(Environment.GetResourceString("Format_IndexOutOfRange")); if (fi.index >= args.Length) throw new FormatException("Format_IndexOutOfRange"); object arg = args[fi.index]; string s = null; if (customFormatter != null) { s = customFormatter.Format(fi.format, arg, formatProvider); } if (s == null) { if (arg is IFormattable) { s = ((IFormattable)arg).ToString(fi.format, formatProvider); } else if (arg != null) { s = arg.ToString(); } } if (s == null) s = String.Empty; int pad = fi.width - s.Length; if (!fi.justify && pad > 0) sb.Append(' ', pad); sb.Append(s); if (fi.justify && pad > 0) sb.Append(' ', pad); } } return sb.ToString(); } //alternate implementation (for comparative testing) // my own test call String.Format() separately: I don't use this. But it's useful to see // how my format method fits. public string OriginalFormat(params Object[] args) { return String.Format(formatProvider, format, args); } 

补充说明:
我担心为我的构造函数提供源代码,因为我不确定依赖于原始.Net实现的许可影响。 但是,任何想要测试它的人都可以公开相关的私有数据并分配模仿特定格式字符串的值。

此外,如果有人提出可以改善构建时间的建议,我非常愿意更改FormatInfo类甚至parts列表。 由于我主要关心的是从前到下的迭代时间,因此LinkedList会更好吗?

[更新]:
嗯……我可以尝试的其他方法是调整我的测试。 我的基准测试非常简单:将名称组合成"{lastname}, {firstname}"格式,并根据区号,前缀,数字和扩展组件组成格式化的电话号码。 这些都没有太多的字符串文字。 当我考虑原始状态机解析器如何工作时,我认为这些字符串文字正是我的代码最好的机会,因为我不再需要检查字符串中的每个字符。

另一个想法:
这个类仍然有用,即使我不能让它变得更快。 只要性能不比基础String.Format()差,我仍然创建了一个stronly类型的接口,允许程序在运行时组装它自己的“格式字符串”。 我需要做的就是提供对零件清单的公共访问。

这是最终结果:

我将基准测试中的格式字符串更改为更适合我的代码的内容:

快速的褐色{0}跳过了懒惰的{1}。

正如我所料,与原版相比,这种情况要好得多; 此代码在5.3秒内完成200万次迭代,而String.Format则为6.1秒。 这是一个不可否认的改进。 您甚至可能会开始使用它作为许多String.Format情况的简单替代品。 毕竟,你不会做更糟糕的事情,你甚至可以获得一个小的性能提升:14%,这没什么可打喷嚏的。

除此之外。 请记住,在专门设计为支持此代码的情况下,我们仍然会在200 万次尝试中谈论不到半秒的差异。 甚至没有繁忙的ASP.Net页面可能会产生那么大的负担,除非你有幸在Top100网站上工作。

最重要的是,这省略了一个重要的替代方案:您可以每次创建一个新的StringBuilder并使用原始的Append()调用手动处理您自己的格式。 通过这种技术,我的基准测试3.9秒内完成 这是一个更大的进步。

所以最后,如果你遇到性能问题的情况,那么有更好的选择。 如果没关系,您可能希望坚持使用简单的内置方法的清晰度。

别现在停下来!

您的自定义格式化程序可能只比内置API稍微高效,但您可以向自己的实现添加更多function,以使其更有用。

我在Java中做了类似的事情,这里有一些我添加的function(除了预编译的格式字符串):

1)format()方法接受varargs数组或Map(在.NET中,它是一个字典)。 所以我的格式字符串可能如下所示:

 StringFormatter f = StringFormatter.parse( "the quick brown {animal} jumped over the {attitude} dog" ); 

然后,如果我已经在地图中有我的对象(这很常见),我可以像这样调用格式方法:

 String s = f.format(myMap); 

2)我有一个特殊的语法,用于在格式化过程中对字符串执行正则表达式替换:

 // After calling obj.toString(), all space characters in the formatted // object string are converted to underscores. StringFormatter f = StringFormatter.parse( "blah blah blah {0:/\\s+/_/} blah blah blah" ); 

3)我有一个特殊的语法,允许格式化检查null-ness的参数,根据对象是null还是非null应用不同的格式化程序。

 StringFormatter f = StringFormatter.parse( "blah blah blah {0:?'NULL'|'NOT NULL'} blah blah blah" ); 

你还可以做很多其他事情。 我的待办事项列表中的任务之一是添加一种新语法,您可以通过指定要应用于每个元素的格式化程序以及要在所有元素之间插入的字符串来自动格式化列表,集和其他集合。 这样的东西……

 // Wraps each elements in single-quote charts, separating // adjacent elements with a comma. StringFormatter f = StringFormatter.parse( "blah blah blah {0:@['$'][,]} blah blah blah" ); 

但语法有点尴尬,我还没有爱上它。

无论如何,重点是您的现有类可能没有框架API更高效,但如果您扩展它以满足您的所有个人字符串格式需求,最终可能会得到一个非常方便的库。 就个人而言,我使用自己的这个库版本来动态构建所有SQL字符串,错误消息和本地化字符串。 这非常有用。

在我看来,为了获得实际的性能提升,您需要将customFormatter和formattable参数所做的任何格式分析分解为一个函数,该函数返回一些数据结构,告诉稍后的格式化调用该做什么。 然后在构造函数中提取这些数据结构并存储它们以供以后使用。 据推测,这将涉及扩展ICustomFormatter和IFormattable。 似乎不太可能。

你是否也考虑过JIT编译的时间? 毕竟,框架将是ngen’d, 可以解释差异?

该框架提供了格式方法的显式覆盖,这些方法采用固定大小的参数列表而不是params object []方法来消除分配和收集所有临时对象数组的开销。 您可能也想考虑代码。 此外,为常见值类型提供强类型重载会减少装箱开销。

我必须相信,花费尽可能多的时间来优化数据IO会获得指数级更高的回报!

这肯定是YAGNI亲此表达的亲戚。 避免过早优化。 APO。