我正在和ReadOnlySpan
一起玩,我希望自己看到它比使用字符串更快但是...到目前为止,它已经不。我知道我的代码可能犯了一个错误,但我无法找到它。
static int CountCharacterWithoutSpan(string originalString, string sequence)
{
int count = 0;
for (int i = 0, length = originalString.Length - sequence.Length; i < length; ++i)
{
if (originalString.Substring(i, sequence.Length).Equals(sequence))
{
count++;
}
}
return count;
}
static int CountCharacterWithSpan(ReadOnlySpan<char> originalString, string sequence)
{
int count = 0;
for (int i = 0, length = originalString.Length - sequence.Length; i < length; ++i)
{
if (originalString.Slice(i, sequence.Length).SequenceEqual(sequence))
{
count++;
}
}
return count;
}
基本上,这段代码的目标是能够在另一个内部找到一个字符串。两者之间的区别在于我使用Slice
而不是Substring
和SequenceEqual
而不是Equals
。但是,当我使用Stopwatch
运行和监控此代码时,CountCharacterWithSpan
总是比CountCharacterWithoutSpan
多2至3倍(字符串测试大约为80K字符)。
我认为问题来自SequenceEquals
,但这是我发现比较切片ReadOnlySpan
和常规字符串(Equals
不起作用的唯一方法,==
1}}更快,但比较参考,所以结果不正确)
答案 0 :(得分:2)
与您在问题中所说的相反,基于跨度的版本实际上比不基于跨度的版本要快。
根据morten-mertner在评论中的建议,我对您的第二种方法进行了稍微修改:
public static int CountCharacterWithSpan(
ReadOnlySpan<char> originalString, ReadOnlySpan<char> sequence)
{
int count = 0;
for (int i = 0, length = originalString.Length - sequence.Length; i < length; ++i)
{
if (originalString.Slice(i, sequence.Length).SequenceEqual(sequence))
{
count++;
}
}
return count;
}
但是,正如我们将看到的,这没有什么区别。它的速度与原始的基于span的速度差不多,并且都比不基于span的速度快得多。
这是BenchmarkDotNet为这三个报告的内容,它们使用在.NET Core 2.2上运行的80K字符originalString
和20个字符sequence
,每个都有三个变体。在“ Random”变体中,sequence
只是随机文本,因此可以很早地检测到没有匹配项。在“匹配”变体中,sequence
是一个确实存在于文本中某个地方的子字符串,但是输入仍然是随机的,因此大多数搜索都非常快速地终止,但是搜索会很慢。在'MatchAll'情况下,originalString
和sequence
一遍又一遍都是相同的字符,这意味着每次比较都会成功,这意味着可能会进行最多的比较工作。 (它将需要反复比较每个字符。)
| Method | Mean | Error | StdDev |
|---------------------------- |-----------:|-----------:|-----------:|
| OriginalWithoutSpanRandom | 1,087.1 us | 11.4152 us | 10.6778 us |
| OriginalWithoutSpanMatch | 1,098.8 us | 26.0405 us | 23.0842 us |
| OriginalWithoutSpanMatchAll | 1,164.3 us | 15.8291 us | 14.8066 us |
| OriginalWithSpanRandom | 188.8 us | 1.3194 us | 1.2341 us |
| OriginalWithSpanMatch | 188.3 us | 0.6132 us | 0.5736 us |
| OriginalWithSpanMatchAll | 224.3 us | 3.0027 us | 2.8087 us |
| ModifiedWithSpanRandom | 189.0 us | 0.9979 us | 0.9334 us |
| ModifiedWithSpanMatch | 189.5 us | 1.1694 us | 1.0367 us |
| ModifiedWithSpanMatchAll | 223.2 us | 1.3251 us | 1.2395 us |
以下是将sequence
更改为200个字符的结果:
| Method | Mean | Error | StdDev |
|---------------------------- |-----------:|----------:|----------:|
| OriginalWithoutSpanRandom | 2,432.2 us | 35.777 us | 31.715 us |
| OriginalWithoutSpanMatch | 2,476.1 us | 42.809 us | 35.747 us |
| OriginalWithoutSpanMatchAll | 2,815.6 us | 22.508 us | 19.953 us |
| OriginalWithSpanRandom | 190.2 us | 1.531 us | 1.432 us |
| OriginalWithSpanMatch | 189.8 us | 1.937 us | 1.717 us |
| OriginalWithSpanMatchAll | 602.3 us | 4.662 us | 4.361 us |
| ModifiedWithSpanRandom | 190.1 us | 2.200 us | 2.058 us |
| ModifiedWithSpanMatch | 191.1 us | 2.860 us | 2.675 us |
| ModifiedWithSpanMatchAll | 599.9 us | 3.696 us | 3.457 us |
这是我们将sequence
更改为2000个字符的样子:
| Method | Mean | Error | StdDev |
|---------------------------- |------------:|-----------:|-----------:|
| OriginalWithoutSpanRandom | 16,819.9 us | 310.576 us | 290.513 us |
| OriginalWithoutSpanMatch | 17,148.8 us | 231.140 us | 216.209 us |
| OriginalWithoutSpanMatchAll | 21,817.9 us | 246.378 us | 218.408 us |
| OriginalWithSpanRandom | 184.2 us | 1.633 us | 1.528 us |
| OriginalWithSpanMatch | 185.3 us | 1.440 us | 1.347 us |
| OriginalWithSpanMatchAll | 4,649.7 us | 22.810 us | 20.221 us |
| ModifiedWithSpanRandom | 185.2 us | 1.198 us | 1.120 us |
| ModifiedWithSpanMatch | 186.7 us | 2.158 us | 2.019 us |
| ModifiedWithSpanMatchAll | 4,651.1 us | 25.013 us | 22.173 us |
您可以看到,我无法重现您描述的结果,其中“ CountCharacterWithSpan
总是比CountCharacterWithoutSpan
多花2至3倍”。在这些测试中,CountCharacterWithoutSpan
始终比任何基于ReadOnlySpan<char>
的版本都要慢得多。 (尽管两者之间的差异很小,无法测量。)
使用两种基于span的方法时,每个比较中完成的工作量都非常大:您可以看到测试之间的实质性差异,其中大多数字符串比较可以在一个或两个字符后退出,而在它必须比较每个字符。 (尽管Random
和Match
示例之间没有有意义的区别-似乎所有比较早被保释和早期保释一次的成本差异很小。这并不奇怪,因为我们基本上是在比较80,000个中的一个比较昂贵,而其余的比较便宜。
这里绝对清楚的是,非基于跨度的版本很昂贵。杀死Substring
的原因是它。在大多数比较几乎立即失败的测试中,这尤其糟糕:您为originalString
的某个子字符串分配了2,000个字符的副本,然后只查看了少数几个字符。
请注意,在我们能够提早保释的情况下,基于跨度的版本的性能几乎与sequence
的长度无关-在所有情况下均为190us。这就是您希望的-在可以确定很早就没有匹配的情况下,sequence
的长度实际上并不重要,但是在您的非基于跨度的版本中,{ {1}}在这些情况下也很重要。
您要进行多少次测量?我想知道您是否只是在衡量一次运行,在这种情况下,您并没有真正在衡量代码运行所需的时间:您主要是在衡量JIT编译器编译它所花费的时间。>