有没有人知道更快的方法来做String.Split()?

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

values = line.Split(delimiter); 

其中line是一个字符串,用于保存由分隔符分隔的值。

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

谢谢!

需要指出的是,如果你遇到文件中的逗号, split()是一种解析CSV文件的可疑方法,例如:

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

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

话虽这么说, split()是为了处理正则表达式而构建的,这显然比你需要的更复杂(并且无论如何都是错误的工具来处理转义的逗号)。 此外, split()会创建大量临时对象。

因此,如果你想加速它(我很难相信这部分的性能确实是一个问题)那么你想要手工完成它并且你想重用你的缓冲区对象,所以你不是经常创建对象和给予垃圾收集器的工作是清理它们。

该算法相对简单:

  • 停在每个逗号;
  • 当你点击引号继续,直到你达到下一组报价;
  • 处理转义引号(即\“)并且可以说是转义逗号(\,)。

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

所以,如果你真的想要表现,那么现在是时候解析了。

或者,更好的是,使用其他人的优化解决方案,如快速CSV阅读器 。

顺便说一下,虽然这与Java有关,但它涉及正则表达式(通用)和replaceAll()与手动编码循环的性能: 将char放入每个N个字符的java字符串中 。

string.Split的BCL实现实际上非常快,我在这里做了一些测试,试图预先形成它并不容易。

但是有一件事你可以做,那就是将它作为一个生成器来实现:

 public static IEnumerable 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更快,但是当它找到它时返回结果,这是懒惰评估的强大function。 如果你排长队或需要节省内存,这就是你要走的路。

上面的方法受到IndexOf和Substring的性能的限制,它执行过多的范围检查索引,并且需要更快地优化它们并实现自己的辅助方法。 你可以击败string.Split性能,但它会采取切割机int-hacking。 你可以在这里阅读我的post。

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

希望这可以帮助

我发现这个实现比Dejan Pelzel的博客快了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]); } 

这种方法有优点和缺点。

你可能认为有优化,但现实将是你将在其他地方支付。

例如,您可以自己进行拆分并遍历所有字符并在遇到它时处理每个列,但无论如何,您将在长时间内复制字符串的所有部分。

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

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

我被告知,90%的CPU时间花费在10%的代码上。 这个“真相”有变化。 在我看来,如果处理CSV是您的应用需要做的事情,那么在斯普利特花费66%的时间并不是那么糟糕。

戴夫

对String.Slit()vs Regex和其他方法进行了一些非常彻底的分析。

我们正在谈论节省非常大的字符串。

String.Split的主要问题(?)是它的一般性,因为它满足了许多需求。

如果你比分析更了解你的数据,它可以改进你自己的。

例如,如果:

  1. 你不关心空字符串,所以你不需要处理那些特殊的方法
  2. 您不需要修剪字符串,因此您无需对其进行任何操作
  3. 您无需检查引用的逗号或引号
  4. 您根本不需要处理报价

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

话虽如此,你应该问的第一个问题是,这实际上是否值得解决。 读取和导入文件所花费的时间是否真的让您觉得这可以很好地利用您的时间? 如果没有,那么我会不管它。

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

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

为了做到正确,CSV解析实际上是非常复杂的,我使用基于包装ODBC Text驱动程序的类是我必须这样做的唯一时间。

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

我完全建议你做一些关于CSV解析的研究,然后再走近一条几乎没有完全正常工作的路径(太常见了)。 只有双引号字符串需要它的Excel是我经验中最难处理的事情之一。

正如其他人所说, 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阅读框架将处理所有这些,并且也非常有效:

Sebastien Lorien的LumenWorks.Framework.IO.Csv

这是我的解决方案:

 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,而split占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,而split占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而不是使用列表……原因是,列表确实没有在我的基准测试中提供任何速度差异所以我回到了“简单”的方式。

  public static unsafe List SplitString(char separator, string input) { List result = new List(); 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; } 

您可以假设String.Split将接近最优; 即它可能很难改进。 到目前为止,更容易的解决方案是检查是否需要拆分字符串。 您很可能直接使用单个字符串。 如果你定义一个StringShim类(引用String,开始和结束索引),你将能够将一个字符串拆分成一组填充程序。 这些将具有小的固定大小,并且不会导致字符串数据副本。

String.split相当慢,如果你想要一些更快的方法,你可以去。 🙂

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

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

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

 private static final String[] fSplit(String src, char delim) { ArrayList output = new ArrayList(); 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 output = new ArrayList(); 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()]); }