性能问题:与String.Format比较

时间:2009-04-17 16:23:38

标签: c# performance string

一段时间后,Jon Skeet的帖子在我的脑海中构建了一个构建CompiledFormatter类的想法,用于循环而不是String.Format()

这个想法是调用格式字符串花费String.Format()的部分是开销;我们应该能够通过将代码移到循环之外来提高性能。当然,诀窍是完全匹配String.Format()行为的新代码。

本周我终于做到了。我使用.Net framework source provided by Microsoft直接修改了他们的解析器(事实证明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<FormatItem> parts = new List<FormatItem>(); 
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 < 0)
            sb.Append(fi.value);
        else
        {
            //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(),我仍然创建了一个强类型接口,允许程序在运行时组装它自己的“格式字符串”。我需要做的就是提供对零件清单的公共访问。

6 个答案:

答案 0 :(得分:8)

这是最终结果:

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

  

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

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

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

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


总之,如果性能无关紧要,那么您应该坚持内置选项的清晰度和简洁性。但是,在分析显示这确实在推动您的表现的情况下,通过StringBuilder.Append()可以获得更好的替代方案。

答案 1 :(得分:3)

现在不要停止!

您的自定义格式化程序可能只比内置API略高一些,但您可以为自己的实现添加更多功能,以使其更有用。

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

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字符串,错误消息和本地化字符串。这非常有用。

答案 2 :(得分:1)

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

答案 3 :(得分:0)

你是否也考虑过编写JIT的时间?毕竟,该框架将是可以解释差异的框架吗?

答案 4 :(得分:0)

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

答案 5 :(得分:0)

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

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