我正在读取一个CSV文件的每一行,需要获取每个列中的值。目前我只是使用以下代码:
values = line.Split(delimiter);
这里的 line
是一个包含由定界符分隔的值的字符串。
在测量我的 ReadNextRow
方法的性能时,我注意到它花费了66% 的时间在 String.Split
上,所以我想知道是否有更快的方法来实现这个功能。
谢谢!
我正在读取一个CSV文件的每一行,需要获取每个列中的值。目前我只是使用以下代码:
values = line.Split(delimiter);
这里的 line
是一个包含由定界符分隔的值的字符串。
在测量我的 ReadNextRow
方法的性能时,我注意到它花费了66% 的时间在 String.Split
上,所以我想知道是否有更快的方法来实现这个功能。
谢谢!
BCL实现的string.Split实际上非常快,我在这里进行了一些测试,试图超越它并不容易。
但有一件事情你可以做,那就是将其实现为生成器:
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的性能,但需要使用巧妙的整数技巧。您可以在这里阅读我的帖子。
需要指出的是,在解析CSV文件时,split()
方法存在疑问,如果文件中出现逗号,可能会导致错误。
1,"Something, with a comma",2,3
不知道您如何进行性能剖析,但我想指出的是要小心剖析这种低级别的细节。Windows/PC计时器的粒度可能会影响到性能,在循环中可能会产生显著的开销,因此需要使用某种控制值。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;
根据用途,你可以使用Pattern.split代替String.split来加快速度。如果你在循环中使用此代码(我假设你可能正在从文件中解析行),String.split(String regex)将在每次循环执行该语句时调用Pattern.compile对你的正则表达式字符串进行编译。为了优化这个问题,应该在循环外部一次性地编译模式,然后在循环内部使用Pattern.split来分割你想要分割的行。
希望这有所帮助。
考虑到这一点,我开始创建一个字符串分割器,它将类似于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!");
}
var splitter = new StringSplitter(2);
var len = splitter.Split("Hello World", ' ');
for (int i = 0; i < len; i++)
{
Console.WriteLine(splitter.Results[i]);
}
这种方法有优点和缺点。
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;
}
你可能认为有一些优化可以实现,但实际上你会在其他地方付出代价。
例如,你可以自己进行分割并遍历所有字符,并在遇到每个列时处理它,但从长远来看,你仍然会复制字符串的所有部分。
例如,在C或C++中,我们可以做的一个优化是用'\0'字符替换所有分隔符,并保留指向列开头的指针。然后,我们就不必复制整个字符串数据才能获取其中的一部分。但这在C#中是无法做到的,也不应该这样做。
如果源中的列数与所需列数之间存在很大差异,则手动遍历字符串可能会产生一些好处。但这种好处将花费您开发和维护的时间。
我听说90%的CPU时间花费在10%的代码上。这个“真理”有各种变化。在我看来,如果处理CSV是您的应用程序需要做的事情,那么在Split中花费66%的时间并不算太糟糕。
Dave
这是我的解决方案:
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
FastSplit花费了7,而split花费了9
FastSplit花费了8,而split花费了10
FastSplit花费了10,而split花费了1405
FastSplit花费了9,而split花费了11
FastSplit花费了8,而split花费了10
此外,我知道有人会反对我使用ReDim Preserve而不是使用列表...原因是,在我的基准测试中,列表并没有提供任何速度差异,所以我回到了“简单”的方式。
正如其他人所说,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读取框架将处理所有这些问题,并且非常高效:
关于String.Slit()与Regex及其他方法的深入分析。
我们正在讨论在非常大的字符串上节省毫秒级时间。