为什么我的ReadOnlySpan <char>比我的字符串慢?

时间:2018-06-16 00:03:58

标签: .net-core

我正在和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而不是SubstringSequenceEqual而不是Equals。但是,当我使用Stopwatch运行和监控此代码时,CountCharacterWithSpan总是比CountCharacterWithoutSpan多2至3倍(字符串测试大约为80K字符)。

我认为问题来自SequenceEquals,但这是我发现比较切片ReadOnlySpan和常规字符串(Equals不起作用的唯一方法,== 1}}更快,但比较参考,所以结果不正确)

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'情况下,originalStringsequence一遍又一遍都是相同的字符,这意味着每次比较都会成功,这意味着可能会进行最多的比较工作。 (它将需要反复比较每个字符。)

|                      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的方法时,每个比较中完成的工作量都非常大:您可以看到测试之间的实质性差异,其中大多数字符串比较可以在一个或两个字符后退出,而在它必须比较每个字符。 (尽管RandomMatch示例之间没有有意义的区别-似乎所有比较早被保释和早期保释一次的成本差异很小。这并不奇怪,因为我们基本上是在比较80,000个中的一个比较昂贵,而其余的比较便宜。

这里绝对清楚的是,非基于跨度的版本很昂贵。杀死Substring的原因是它。在大多数比较几乎立即失败的测试中,这尤其糟糕:您为originalString的某个子字符串分配了2,000个字符的副本,然后只查看了少数几个字符。

请注意,在我们能够提早保释的情况下,基于跨度的版本的性能几乎与sequence的长度无关-在所有情况下均为190us。这就是您希望的-在可以确定很早就没有匹配的情况下,sequence的长度实际上并不重要,但是在您的非基于跨度的版本中,{ {1}}在这些情况下也很重要。

您要进行多少次测量?我想知道您是否只是在衡量一次运行,在这种情况下,您并没有真正在衡量代码运行所需的时间:您主要是在衡量JIT编译器编译它所花费的时间。