【问题标题】:Should I roll my own version of ParseInt32?我应该推出自己的 ParseInt32 版本吗?
【发布时间】:2009-01-06 11:30:58
【问题描述】:

我正在编写一个高性能解析器,在我看来Int32.Parse 可能太慢了。我写了一个假设正确输入的简单版本,它的性能要好得多。那么我应该创建自己的版本吗?还是已经有另一种更快的方法可用?

我的方法是这样的:

// parse simple int, assuming relatively correct input (i.e. all digits)
public static int ParseInt32Simply(string str) {
    if (str == null) throw new ArgumentNullException("str");
    if (str.Length == 0) throw new ArgumentException("str is empty");

    int sign = 1, index = 0;
    if (str[0] == '-') { sign = -1; index = 1; }
    else if (str[0] == '+') { index = 1; }

    int result = 0;
    for (; index < str.Length; ++index) {
        result = 10 * result + (str[index] - '0');
    }

    if (result < 0) throw new OverflowException(str + " is too large for Int32");

    return result * sign;
}

我的结果与内置的等效结果非常不同:

Int32.Parse      took 8.2775453 seconds
ParseInt32Simply took 0.6511523 seconds
Int32.Parse      took 6.7625807 seconds
ParseInt32Simply took 0.4677390 seconds

(在我的机器上运行 2500 万次迭代;P4 3 GHz,运行 VS 2008 SP1)

那么,我应该使用我的版本吗?或者我可以使用其他方法吗?

【问题讨论】:

  • 由于整数运算本质上是未经检查的,所以运行时不会自动抛出溢出异常吗?即使没有,您的溢出检查不会只检测到一半的情况吗? (即没有飞回正数时)
  • 对不起,我的意思是含蓄的,而不是固有的
  • 请注意您的分析不会扭曲结果。如果您调用一个函数的次数足够多,则结果可能会因函数调用开销而出现偏差。
  • @DrJokepu:正如你所说,它是“未经检查的”,但这意味着运行时不会抛出异常(与“检查”相反)。至于一半的情况,我知道这一点。我会以不同的方式处理它,但我会保持示例简洁。
  • @cletus:我同时调用了我的函数和 Int32.Parse,所以函数调用开销是一样的。

标签: .net performance parsing


【解决方案1】:

您是否分析过您的代码以确定 ParseInt32 实际上是瓶颈?我不会替换你正在编码的环境的“标准库”的一部分,除非你确定你会看到好处。

【讨论】:

  • 我把这两种方法相互对照运行,得到了上面提到的计时结果。
  • 我的意思是您正在编写的配置文件应用程序,而不仅仅是这个特定的功能。这样,您可以找出程序中的瓶颈是 Int32.Parse 还是其他地方。您可能会花费大量时间进行其他处理。见en.wikipedia.org/wiki/Performance_analysis
  • 嗯,我认为应用程序现在不能再优化了,除了很少的部分。但是 Int32.Parse 似乎确实占了很大比例(尽管我没有使用分析器来检验我的假设)。
  • Hosam,回答您的问题的唯一方法是使用分析器来证明这将以可衡量的方式使您的应用程序受益。你要解析 2500 万个整数吗?
  • 我正在解析 1.5 - 200 万条消息,其中包含许多整数(日期、时间、整数等),因此我认为我的解析器将从增加的性能中受益匪浅。但是,我希望我能找到一个我可以使用的库,而不是创建我自己的库! :(
【解决方案2】:

在 .net 中,Int32.Parse 非常非常快,当它成功时。

当它失败时,它会抛出一个异常 - 因为异常很慢,所以它非常慢。

您需要扩展您的测试 - 您需要检查与您需要它执行的操作相似的好坏字符串模式的时间。

如果你很确定你的所有字符串都是有效的整数,那么Int32.Parse 就是要走的路。如果您怀疑只有很少一部分是有效的,那么在循环中使用 Int32.TryParse 而不是 try-catch 会快得多。

如果您的 try-catch 在循环之外,则通常使用 Int32.Parse - 您将收到异常并在第一次获得无效值时停止。

如果您的try-catch 在循环内,请改用Int32.TryParse

Int32.ParseInt32.TryParse 都进行了高度优化,并且相对成熟 - 我预计它们很难改进,除非您有专门的情况。

【讨论】:

  • 我只使用成功的案例来运行我的测试,这应该是 Int32.Parse 的快捷方式。但是,与我的方法相比,它仍然太慢了。
  • Int32.Parse 还处理本地化 - 美国用户可以解析“10,000”,法国用户可以解析“10 000”。如果你不需要这些,那就去吧。
【解决方案3】:

我的观点是,如果您节省的时间很重要并且对您的应用程序有益,那么就去做吧。

我们在 XML 解析方面遇到了一个类似的问题,出于性能原因,我们选择手动进行,但它基于已知环境 - 我们正在提供 XML,因此我们可以相当安全地在解析中使用快捷方式。

显然,风险在于它不太可能像标准库版本那样完整,因此团队的新开发人员需要意识到这一点,以免他们做出破坏它的事情。

【讨论】:

    【解决方案4】:

    是的 - 只要您 100% 确定源数据是您可以控制的(因此始终符合您的 Int32 格式),您就可以使用您自己的解析 int 版本。此外,您应该使用与世界其他地方隔离的自己的代码,因为如果您在要发布的某个库中拥有此代码,人们可能希望拥有 Int32.Parse 的标准行为。如果你不能提供,那对他们没有好处。但是,正如这里的许多人所建议的那样,如果您想充分发挥自己的性能,那么您应该确定这是真正需要做的事情。但是,您可能比这里的任何人都更了解自己的代码。

    我个人会尽量避免更改解析。如果还有其他瓶颈,那么可能值得首先调查。

    【讨论】:

    • Int32.Parse的问题在于它使用了通用的解析方法(内部System.Number.ParseNumber),可以解析任何数字形式。这肯定比仅仅解析一个 int 慢得多。即使我扩展我的方法来处理更多的情况,它消耗的时间也只是原来的两倍。
    【解决方案5】:

    如果您的测试是可验证的,并且您确实需要性能提升(例如,您以每秒数万次的速度调用该函数),那么就去做吧。

    我只想更改名称...因为 ParseInt32Simply 不会告诉维护程序员任何事情。我认为像 TrustedSourceInt32ParseGuaranteedInt32Parse 这样的名称或类似的名称是更好的名称。

    【讨论】:

    • 感谢您的建议。我想不出一个更好的方法来命名我的方法。 TrustedSourceInt32Parse 看起来不错,但是太长了!欢迎任何更多建议。 :)
    • 实际上,我想如果您使用扩展方法扩展 Int32,您可以简单地使用“TrustedSourceParse”......例如int.TrustedSourceParse("1000");
    • 嗯,这是不可能的,因为这将是一个静态方法,而不是扩展。 :)
    【解决方案6】:

    我认为这里的主要问题是您的句子假设输入正确。通过阅读您的代码,它似乎无法正确处理“12x”。

    Int32.Parse 会做很多事情来验证输入,甚至可能会记录您的文化以处理一些文化差异,尽管我想不出任何专门针对 Int32 的事情。

    您确定瓶颈是您代码中的 Int32?

    【讨论】:

    • 为了提高性能,做了这个假设。我从一个受信任的来源接收输入,所以我正在简化一些条件(因此命名为“Simply”)。方法文档中有“12x”等情况无法正确处理。
    • 我知道 Int32.Parse 做了很多事情,这就是为什么我想自己动手。 (实际上我认为它在最常见的情况下比它应该做的要多得多。您可以检查内部 System.Number.ParseNumber 方法。)
    • 从上面的计时结果可以看出,我的方法和 Int32.Parse 的计时有很大的不同。对于我的解析器来说,这可能是 5 倍的速度提升!
    【解决方案7】:

    如果您解析一种您知道是有效数字的格式,您确实可以编写一个更快的自定义解析器。我曾经为了同样的目的写过一个 Double.Parse 函数。并且从最低有效数字开始更快。这样你就可以增加你解析的数字的力量。

    我已经创建了一个快速实现,

    public static Int32 ParseValidNumberAsInt32(string str)
    {
        if (str == null) 
            throw new ArgumentNullException("str");
        if (str.Length == 0) 
            throw new ArgumentException("str is empty");
        Int32 result = 0;
        Int32 currentPower = 1;
        Boolean isNegative = str[0] == '-';
    
        for (int currentCharIndex = str.Length - 1; currentCharIndex > 0; currentCharIndex--)
        {
            result += (str[currentCharIndex] - '0') * currentPower;
            currentPower *= 10;
        }
        return isNegative ? -1 * result : result + ((str[0] - '0') * currentPower);
    }
    

    如果你真的想要速度,你可以写一个不安全的实现..

    如果您解析一个大文件,您可以将文件作为原始字节读取并使用它们。这将使它更快(不转换为 unicode 字符串,不将字符串拆分为行,不拆分子字符串中的行,不解析子字符串),但是您将失去可维护性。

    【讨论】:

    • 非常感谢您分享您的实现。我不确定从最低有效数字开始如何比其他方式更快。但我会检查一下。
    • 至于不安全的建议,没有区别。实际上在某些情况下它比正常的慢,可能是因为它需要额外的指令来“固定”字符串(使用“固定”)。
    • 我实际上是从 StreamReader 读取(包裹在套接字上),但由于我正在读取 ASCII,那么您关于使用原始字节的观点可能会使其更快。我会尝试一下,看看我得到了什么。非常感谢!
    • 我想过为什么我从最低有效数字开始,我认为这是因为我正在使用浮点数并希望将浮点数学保持在最低限度(所以要跟踪功率而不是仅仅乘以前一个数字)。我的估计是 str[i] - '0' 会更重。
    • 确实,使用原始字节会更快,您可以使用 result += (data[i] - 0x30) * currentPower;而且我怀疑 IL 更干净.. 使用像样的分析器,您可以查明最大瓶颈的代码行,并尝试进一步优化..
    【解决方案8】:

    你如何测量速度?我试过这个:

    Stopwatch sw = new Stopwatch();
    Random rand = new Random();
    
    for (int n = 0; n < 10; n++)
    {
        sw.Start();
        for (int i = 0; i < 1000000; i++)
        {
            ParseInt32Simply(rand.Next().ToString());
        }
        sw.Stop();
        Console.WriteLine(sw.Elapsed.Ticks + " - ParseInt32Simply");
        sw.Reset();
    
        sw.Start();
        for (int i = 0; i < 1000000; i++)
        {
            int.Parse(rand.Next().ToString());
        }
        sw.Stop();
        Console.WriteLine(sw.Elapsed.Ticks + " - int.Parse");
        sw.Reset();
        Console.WriteLine();
    }
    

    结果完全不同:

    2932852 - ParseInt32Simply
    4684522 - int.Parse

    3003988 - ParseInt32Simply
    4666928 - int.Parse

    2892545 - ParseInt32Simply
    4660209 - int.Parse

    2888998 - ParseInt32Simply
    4636007 - int.Parse

    2955727 - ParseInt32Simply
    4668501 - int.Parse

    2929210 - ParseInt32Simply
    4653799 - int.Parse

    2893706 - ParseInt32Simply
    4671503 - int.Parse

    2899547 - ParseInt32Simply
    4633957 - int.Parse

    您的简单方法仍然更快,但不到 2 倍(实际上这是非常好的性能!)。

    【讨论】:

    • 尝试创建另一个循环,只执行 rand.Next().ToString()。您知道,这可能会使该分数增加 2,500,000 :) 在这种情况下,ParseInt32Simply 的速度要快 2,000,000 倍:P
    • 我正在运行类似的测试,但使用的是静态字符串(而不是 rand,以便获得更准确的计时)。但我得到了我在问题中写的结果。
    【解决方案9】:

    查看此博客条目:Karl Seguin 的 Fast string to integer conversion

    【讨论】:

      【解决方案10】:

      null 和空字符串的验证是不够的,你应该检查参数是否是有效的整数。

      【讨论】:

      • 我通过假设输入正确来简化方法。如果调用者不太确定,那么他应该调用 Int32.Parse。 (我不建议替换 Int32.Parse;我只是让它更简单。)
      【解决方案11】:

      您的测试结果如何? 看来你的测试不行。

      当我循环 50000 次时,我只有很小的差异 然后我有大约 30K 滴答声有利于您的自定义方法, 但这对于普通方法的优点来说是可以忽略的

      【讨论】:

      • 我的测试运行了 2500 万次。我已经更新了我的问题以表明这一点。很抱歉忘记提及。
      猜你喜欢
      • 2010-10-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-01-29
      相关资源
      最近更新 更多