【问题标题】:Does any one know of a faster method to do String.Split()?有谁知道更快的方法来执行 String.Split()?
【发布时间】:2009-02-20 10:00:00
【问题描述】:

我正在读取 CSV 文件的每一行,并且需要获取每列中的各个值。所以现在我只是在使用:

values = line.Split(delimiter);

其中line 是一个字符串,其中包含由分隔符分隔的值。

测量我的ReadNextRow 方法的性能我注意到它在String.Split 上花费了 66%,所以我想知道是否有人知道更快的方法来做到这一点。

谢谢!

【问题讨论】:

  • - 我知道 CSV 文件的确切内容,所以我不必担心转义字符等。- 我使用 JetBrains 的 dotTrace 进行分析。 - 我实际上在代码的其他部分使用 Code Project CsvReader - 性能在这段代码中很重要,这就是我问的原因
  • 感谢所有回复。抱歉,我的评论不正确,因为此评论字段似乎忽略了新行。

标签: c# .net performance string csv


【解决方案1】:

string.Split 的 BCL 实现其实相当快,我在这里做了一些测试,试图超越它,这并不容易。

但是您可以做一件事,那就是将其实现为生成器:

public static IEnumerable<string> GetSplit( this string s, char c )
{
    int l = s.Length;
    int i = 0, j = s.IndexOf( c, 0, l );
    if ( j == -1 ) // No such substring
    {
        yield return s; // Return original and break
        yield break;
    }

    while ( j != -1 )
    {
        if ( j - i > 0 ) // Non empty? 
        {
            yield return s.Substring( i, j - i ); // Return non-empty match
        }
        i = j + 1;
        j = s.IndexOf( c, i, l - i );
    }

    if ( i < l ) // Has remainder?
    {
        yield return s.Substring( i, l - i ); // Return remaining trail
    }
}

上述方法不一定比 string.Split 更快,但它会在找到结果时返回结果,这就是惰性求值的威力。如果您排长队或需要节省内存,这就是您要走的路。

上述方法受限于 IndexOf 和 Substring 的性能,它们执行过多的超出范围检查的索引,为了更快,您需要优化掉这些并实现自己的辅助方法。您可以击败 string.Split 性能,但它需要 cleaver int-hacking。你可以阅读我关于 here 的帖子。

【讨论】:

  • 显然不需要节省内存,但是需要节省CPU。
  • @Dave Van den Eynde - 我认为两者兼顾很重要!但是,是的,大多数程序员都大大忽视了内存优化。
  • 我做了一个类似的方法,它比使用Split的现有算法要慢,但是因为我们处理的是这么大的字符串(多兆字节),它节省了大约30%的RAM消耗.
  • 你知道,代码没有优化,string.Split 更快的原因是因为它使用了不安全的代码。如果您将其包括在此处,则运行时间是相同的。除了这样内存效率更高。
  • 我知道这是旧的,但我想我会指出这个解决方案似乎是从返回的集合中删除空项目。调用 "1,,3".GetSplit(',') 返回一个仅包含 2 个项目的集合。 1 和 3。这是与 .net 的拆分方法不同的行为。
【解决方案2】:

应该指出,split() 是一种有问题的解析 CSV 文件的方法,以防您在文件中遇到逗号,例如:

1,"Something, with a comma",2,3

在不知道您是如何分析的情况下,我要指出的另一件事是在分析这种低级细节时要小心。 Windows/PC 计时器的粒度可能会发挥作用,并且您在循环时可能会有很大的开销,因此请使用某种控制值。

话虽如此,split() 是为处理正则表达式而构建的,这显然比您需要的要复杂得多(无论如何处理转义逗号的工具都是错误的)。此外,split() 创建了许多临时对象。

因此,如果您想加快速度(而且我很难相信这部分的性能确实是一个问题),那么您希望手动完成,并且希望重用缓冲区对象,这样您就不会不断地创建对象并让垃圾收集器清理它们。

算法相对简单:

  • 停在每个逗号处;
  • 当您点击引号时继续,直到您点击下一组引号;
  • 处理转义引号(即 \")和可能转义的逗号 (\,)。

哦,为了让您了解正则表达式的成本,有一个问题(Java 不是 C#,但原理相同)有人想用字符串替换每个第 n 个字符。我建议在字符串上使用replaceAll()。 Jon Skeet 手动编码循环。出于好奇,我比较了这两个版本,他的版本要好一个数量级。

所以如果你真的想要性能,是时候手动解析了。

或者,更好的是,使用其他人的优化解决方案,例如 fast CSV reader

顺便说一下,虽然这与 Java 有关,但它涉及一般正则表达式(这是通用的)和 replaceAll() 与手动编码循环的性能:Putting char into a java string for each N characters

【讨论】:

  • 我已经链接了一个关于字符串替换方法的类似主题的答案,您可以在我自己对这个问题的答案的末尾找到该链接。
  • 我只是想说声谢谢。你重申了我的想法,并迫使我再次检查我的代码并查看我效率低下的地方。原来我有一个错误顺序的条件语句,我想我会在没有看到你的帖子的情况下结束它。
  • 在 excel 生成的 csv 中,转义引号是 "",而不是 \"
  • 现在和 Span 怎么样?
【解决方案3】:

根据使用情况,您可以通过使用 Pattern.split 而不是 String.split 来加快速度。如果您在循环中有此代码(我假设您可能会这样做,因为听起来您正在解析文件中的行) String.split(String regex) 每次循环语句都会在您的正则表达式字符串上调用 Pattern.compile执行。为了优化这一点,Pattern.compile 循环外的模式一次,然后使用 Pattern.split,将要拆分的行传递到循环内。

希望对你有帮助

【讨论】:

    【解决方案4】:

    我发现这个实现比Dejan Pelzel's blog 快了 30%。我从那里引用:

    解决方案

    考虑到这一点,我开始创建一个字符串拆分器,该拆分器将使用类似于 StringBuilder 的内部缓冲区。它使用非常简单的逻辑来遍历字符串并将值部分保存到缓冲区中。

    public int Split(string value, char separator)
    {
        int resultIndex = 0;
        int startIndex = 0;
    
        // Find the mid-parts
        for (int i = 0; i < value.Length; i++)
        {
            if (value[i] == separator)
            {
                this.buffer[resultIndex] = value.Substring(startIndex, i - startIndex);
                resultIndex++;
                startIndex = i + 1;
            }
        }
    
        // Find the last part
        this.buffer[resultIndex] = value.Substring(startIndex, value.Length - startIndex);
        resultIndex++;
    
        return resultIndex;
    

    如何使用

    StringSplitter 类的使用非常简单,您可以在下面的示例中看到。请注意重用 StringSplitter 对象,而不是在循环中或一次性使用它的新实例。在这种情况下,最好使用内置的 String.Split。

    var splitter = new StringSplitter(2);
    splitter.Split("Hello World", ' ');
    if (splitter.Results[0] == "Hello" && splitter.Results[1] == "World")
    {
        Console.WriteLine("It works!");
    }
    

    Split 方法返回找到的项目数,因此您可以轻松地遍历结果,如下所示:

    var splitter = new StringSplitter(2);
    var len = splitter.Split("Hello World", ' ');
    for (int i = 0; i < len; i++)
    {
        Console.WriteLine(splitter.Results[i]);
    }
    

    这种方法有优点也有缺点。

    【讨论】:

    • 虽然理论上这可以回答这个问题,it would be preferable 在这里包含答案的基本部分,并提供链接以供参考。
    【解决方案5】:

    这是一个使用 ReadOnlySpan 的非常基本的示例。在我的机器上,这大约需要 150ns,而 string.Split() 大约需要 250ns。这是一个不错的 40% 改进。

    string serialized = "1577836800;1000;1";
    ReadOnlySpan<char> span = serialized.AsSpan();
    
    Trade result = new Trade();
    
    index = span.IndexOf(';');
    result.UnixTimestamp = long.Parse(span.Slice(0, index));
    span = span.Slice(index + 1);
    
    index = span.IndexOf(';');
    result.Price = float.Parse(span.Slice(0, index));
    span = span.Slice(index + 1);
    
    index = span.IndexOf(';');
    result.Quantity = float.Parse(span.Slice(0, index));
    
    return result;
    

    请注意,ReadOnlySpan.Split() 很快就会成为框架的一部分。看 https://github.com/dotnet/runtime/pull/295

    【讨论】:

    • 非常聪明!我想,正是这种方法所针对的情况类型
    【解决方案6】:

    您可能认为需要进行优化,但实际情况是您需要在其他地方为它们付费。

    例如,您可以“自己”进行拆分并遍历所有字符并在遇到每一列时对其进行处理,但从长远来看,您将复制字符串的所有部分。

    例如,我们可以在 C 或 C++ 中进行的优化之一是将所有分隔符替换为 '\0' 字符,并保留指向列开头的指针。然后,我们不必为了获取其中的一部分而复制所有字符串数据。但这在 C# 中是做不到的,你也不想这样做。

    如果源中的列数与您需要的列数之间存在很大差异,手动遍历字符串可能会产生一些好处。但这种好处会花费您开发和维护它的时间。

    有人告诉我,90% 的 CPU 时间花在了 10% 的代码上。这个“真理”有不同的说法。在我看来,如果处理 CSV 是您的应用需要做的事情,那么将 66% 的时间花在拆分上并没有那么糟糕。

    戴夫

    【讨论】:

      【解决方案7】:

      Some very thorough analysis on String.Slit() vs Regex and other methods.

      不过,我们正在谈论通过非常大的字符串节省 ms。

      【讨论】:

      • 通常我喜欢.Net Perls,但我认为他们的比较是不公平的。如果你知道你会经常使用 Regex,你可以编译它并从循环中提取它。使用该策略可以大大减少总时间。
      • 文章被删除,这是dotnetperls.com上文章的存档版本:web.archive.org/web/20090316210342/http://dotnetperls.com/…
      • 回到 dotnetperls:dotnetperls.com/split 我的发现:10000000 Regex.split's 比 10000000 string.Split's (.net framework 4) 慢 10%
      【解决方案8】:

      String.Split 的主要问题(?)是它的通用性,因为它可以满足许多需求。

      如果您比 Split 更了解自己的数据,则可以改进自己的数据。

      例如,如果:

      1. 您不关心空字符串,因此您无需以任何特殊方式处理它们
      2. 您不需要修剪字符串,因此您无需对这些字符串或周围做任何事情
      3. 您无需检查带引号的逗号或引号
      4. 您根本不需要处理引号

      如果其中任何一个是正确的,您可能会通过编写自己的更具体的 String.Split 版本来看到改进。

      话虽如此,您应该问的第一个问题是这是否真的是一个值得解决的问题。读取和导入文件所花费的时间是否如此之长,以至于您实际上觉得这是一种很好的利用时间?如果没有,那我就别管它了。

      第二个问题是为什么 String.Split 与您的其他代码相比使用了那么多时间。如果答案是代码对数据做的很少,那么我可能不会打扰。

      但是,如果您将数据填充到数据库中,那么您在 String.Split 中花费的代码时间的 66% 就会构成一个大问题。

      【讨论】:

        【解决方案9】:

        CSV 解析实际上非常复杂,我使用了基于包装 ODBC 文本驱动程序的类,这是我唯一一次必须这样做。

        上面推荐的 ODBC 解决方案乍一看基本上是相同的方法。

        我强烈建议您先对 CSV 解析进行一些研究,然后再走上一条几乎但不太可行(太常见)的道路。 Excel 中只有 需要 的双引号字符串,这是我的经验中最难处理的问题之一。

        【讨论】:

          【解决方案10】:

          正如其他人所说,String.Split() 并不总是适用于 CSV 文件。考虑一个如下所示的文件:

          "First Name","Last Name","Address","Town","Postcode"
          David,O'Leary,"12 Acacia Avenue",London,NW5 3DF
          June,Robinson,"14, Abbey Court","Putney",SW6 4FG
          Greg,Hampton,"",,
          Stephen,James,"""Dunroamin"" 45 Bridge Street",Bristol,BS2 6TG
          

          (例如,语音标记的使用不一致,包括逗号和语音标记的字符串等)

          这个 CSV 阅读框架将处理所有这些,而且非常高效:

          LumenWorks.Framework.IO.Csv by Sebastien Lorien

          【讨论】:

            【解决方案11】:

            这是我的解决方案:

            Public Shared Function FastSplit(inputString As String, separator As String) As String()
                    Dim kwds(1) As String
                    Dim k = 0
                    Dim tmp As String = ""
            
                    For l = 1 To inputString.Length - 1
                        tmp = Mid(inputString, l, 1)
                        If tmp = separator Then k += 1 : tmp = "" : ReDim Preserve kwds(k + 1)
                        kwds(k) &= tmp
                    Next
            
                    Return kwds
            End Function
            

            这是一个带有基准测试的版本:

            Public Shared Function FastSplit(inputString As String, separator As String) As String()
                    Dim sw As New Stopwatch
                    sw.Start()
                    Dim kwds(1) As String
                    Dim k = 0
                    Dim tmp As String = ""
            
                    For l = 1 To inputString.Length - 1
                        tmp = Mid(inputString, l, 1)
                        If tmp = separator Then k += 1 : tmp = "" : ReDim Preserve kwds(k + 1)
                        kwds(k) &= tmp
                    Next
                    sw.Stop()
                    Dim fsTime As Long = sw.ElapsedTicks
            
                    sw.Start()
                    Dim strings() As String = inputString.Split(separator)
                    sw.Stop()
            
                    Debug.Print("FastSplit took " + fsTime.ToString + " whereas split took " + sw.ElapsedTicks.ToString)
            
                    Return kwds
            End Function
            

            以下是一些关于相对较小但大小不一的字符串的结果,最多为 8kb 块。 (时间以刻度为单位)

            FastSplit 耗时 8,而拆分耗时 10

            FastSplit 耗时 214,而拆分耗时 216

            FastSplit 耗时 10 而拆分耗时 12

            FastSplit 需要 8 次,而拆分需要 9 次

            FastSplit 耗时 8,而拆分耗时 10

            FastSplit 耗时 10 而拆分耗时 12

            FastSplit 耗时 7,而拆分耗时 9

            FastSplit 耗时 6,而拆分耗时 8

            FastSplit 耗时 5,而拆分耗时 7

            FastSplit 耗时 10,而拆分耗时 13

            FastSplit 耗时 9,而拆分耗时 232

            FastSplit 耗时 7,而拆分耗时 8

            FastSplit 需要 8 次,而拆分需要 9 次

            FastSplit 耗时 8,而拆分耗时 10

            FastSplit 耗时 215,而拆分耗时 217

            FastSplit 耗时 10,而拆分耗时 231

            FastSplit 耗时 8,而拆分耗时 10

            FastSplit 耗时 8,而拆分耗时 10

            FastSplit 耗时 7,而拆分耗时 9

            FastSplit 耗时 8,而拆分耗时 10

            FastSplit 耗时 10,而拆分耗时 1405

            FastSplit 耗时 9,而拆分耗时 11

            FastSplit 耗时 8,而拆分耗时 10

            另外,我知道有人会阻止我使用 ReDim Preserve 而不是使用列表...原因是,列表在我的基准测试中确实没有提供任何速度差异,所以我回到了“简单”的方式.

            【讨论】:

              【解决方案12】:
                  public static unsafe List<string> SplitString(char separator, string input)
                  {
                      List<string> result = new List<string>();
                      int i = 0;
                      fixed(char* buffer = input)
                      {
                          for (int j = 0; j < input.Length; j++)
                          {
                              if (buffer[j] == separator)
                              {
                                  buffer[i] = (char)0;
                                  result.Add(new String(buffer));
                                  i = 0;
                              }
                              else
                              {
                                  buffer[i] = buffer[j];
                                  i++;
                              }
                          }
                          buffer[i] = (char)0;
                          result.Add(new String(buffer));
                      }
                      return result;
                  }
              

              【讨论】:

                【解决方案13】:

                您可以假设 String.Split 将接近最优;即它可能很难改进。到目前为止,更简单的解决方案是检查是否需要拆分字符串。您很可能会直接使用各个字符串。如果您定义一个 StringShim 类(引用 String、begin & end index),您将能够将 String 拆分为一组 shim。这些将具有较小的固定大小,并且不会导致字符串数据复制。

                【讨论】:

                • 一旦您需要将 StringShim 传递给接受字符串的对象,它将导致字符串数据复制。除非您的整个应用都使用 shims。
                • 你根本无法假设。我将使用正则表达式与手动编码来挖掘示例,其中正则表达式解决方案要慢一个数量级。
                • 我的观点是,使用相同的界面很难变得更快。我的 StringShim 解决方案非常明确地更改了 split() 接口以使事情变得更快。
                • 几乎每个 .NET 函数都是为多案例场景而设计的,因此如果您可以确定数据,您可以构建一个定制的函数,该函数的性能总是比默认的 .NET 实现更好。我对你的回答投了反对票,因为重新发明轮子并不总是一件坏事,尽管互联网希望看到你反刍。
                【解决方案14】:

                String.split 比较慢,如果你想要一些更快的方法,就这样吧。 :)

                但是,基于规则的解析器可以更好地解析 CSV。

                这个家伙,已经为 java 制作了一个基于规则的标记器。 (不幸的是需要一些复制和粘贴)

                http://www.csdgn.org/code/rule-tokenizer

                private static final String[] fSplit(String src, char delim) {
                    ArrayList<String> output = new ArrayList<String>();
                    int index = 0;
                    int lindex = 0;
                    while((index = src.indexOf(delim,lindex)) != -1) {
                        output.add(src.substring(lindex,index));
                        lindex = index+1;
                    }
                    output.add(src.substring(lindex));
                    return output.toArray(new String[output.size()]);
                }
                
                private static final String[] fSplit(String src, String delim) {
                    ArrayList<String> output = new ArrayList<String>();
                    int index = 0;
                    int lindex = 0;
                    while((index = src.indexOf(delim,lindex)) != -1) {
                        output.add(src.substring(lindex,index));
                        lindex = index+delim.length();
                    }
                    output.add(src.substring(lindex));
                    return output.toArray(new String[output.size()]);
                }
                

                【讨论】:

                  猜你喜欢
                  • 2013-06-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2022-12-20
                  • 2010-12-04
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多