【问题标题】:Performance issue: comparing to String.Format性能问题:与 String.Format 相比
【发布时间】:2009-04-17 16:23:37
【问题描述】:

不久前 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() 差,我仍然创建了一个强类型接口,它允许程序在运行时组装它自己的“格式字符串”。我需要做的就是提供对零件清单的公共访问权限。

【问题讨论】:

    标签: c# performance string


    【解决方案1】:

    这是最终结果:

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

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

    正如我所料,这比原来的要好得多;此代码在 5.3 秒内完成 200 万次迭代,而 String.Format 则为 6.1 秒。这是不可否认的进步。您甚至可能想开始使用它作为许多String.Format 情况的不费吹灰之力的替代品。毕竟,您的表现不会更差,甚至可能会获得小幅性能提升:高达 14%,这没什么好打喷嚏的。

    除了它是。请记住,在专门设计用于支持此代码的情况下,我们仍然在谈论 2 00 万次尝试的不到半秒的差异。除非您有幸在排名前 100 的网站上工作,否则即使是繁忙的 ASP.Net 页面也不会产生这么大的负载。

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


    总而言之,如果性能不那么重要,您应该坚持内置选项的清晰性和简单性。但是,当分析表明这确实在推动您的表现时,可以通过StringBuilder.Append() 获得更好的替代方案。

    【讨论】:

      【解决方案2】:

      现在不要停下来!

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

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

      1) format() 方法接受可变参数数组或 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) 我有一个特殊的语法,允许格式化检查参数是否为空,根据对象是空还是非空应用不同的格式化程序。

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

      【讨论】:

      • 值得注意的是,6 年后,所有这些都被纳入了框架;)尽管如此,它们仍然不会在循环中预编译格式字符串(即使使用编译器优化也不行)。至少一些幕后代码发生了变化,我可以使用FormattableString 类型来简化我自己的实现。
      【解决方案3】:

      在我看来,为了获得实际的性能改进,您需要将您的 customFormatter 和 formattable 参数完成的任何格式分析分解为一个函数,该函数返回一些数据结构,该数据结构告诉稍后的格式化调用要做什么.然后将这些数据结构拉入构造函数并将它们存储起来以备后用。大概这将涉及扩展 ICustomFormatter 和 IFormattable。似乎不太可能。

      【讨论】:

      • 我认为大多数使用 String.Format() 不涉及自定义格式化程序或格式提供程序。即使您在格式字符串的一部分中使用它,您也可能不会在另一部分中使用它。我对它们的依赖是模仿原始行为。
      • 好的。我真正看到的只是微小的增量事物,例如将您的 fi.index
      • 不要敲它:模板字符串生成器可能最终成为赢家。
      • 好吧,让我知道进展如何。 :)
      【解决方案4】:

      您是否也考虑过进行 JIT 编译的时间?毕竟,框架将是 ngen'd 哪个可以解释差异?

      【讨论】:

      • 我不认为是这样,否则我会看到格式化 10,000 个项目与 1,000,000 个项目之间存在更大的差异。
      • 嗯...这可能会导致这几毫秒,因为无论有多少项目都会发生一次。
      • JIT 编译只发生一次,因此无论输入大小如何,这都可以解释您和框架之间的时间差异。
      • 如果你跑过这一次,然后在第二个循环上做你的计时,它应该处理 JIT 计时问题。
      【解决方案5】:

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

      【讨论】:

      • 否:固定大小的重载只是创建数组并调用泛型重载。存在固定大小重载的原因是允许传入 ProviderInfo 和 Format 字符串参数,而无需将它们包装到 params 参数中。
      【解决方案6】:

      我必须相信,花尽可能多的时间优化数据 IO 会获得成倍增长的回报!

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

      【讨论】:

      • 这有点像是我的“最终结果”帖子的重点——这不值得,除非作为一个有趣的实验:)
      猜你喜欢
      • 2017-11-17
      • 2015-10-04
      • 1970-01-01
      • 2020-12-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-19
      • 2012-05-06
      相关资源
      最近更新 更多