Я играю с 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 всегда занимает в 2–3 раза больше, чем CountCharacterWithoutSpan (проверка строки составляет около 80К символов).
Я думаю, что проблема связана с SequenceEquals, но я нашел единственный способ сравнить нарезанный ReadOnlySpan и обычную строку (Equals не работает, а == быстрее, но сравните ссылку, поэтому результат неверен)





Вопреки тому, что вы сказали в своем вопросе, версия на основе диапазона на самом деле намного быстрее, чем версия на основе диапазона.
Согласно предложению 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;
}
Но, как мы увидим, это не имеет значения. Он примерно такой же быстрый, как и ваш исходный, и оба намного быстрее, чем ваш, не основанный на диапазоне.
Вот что сообщает BenchmarkDotNet для всех трех, используя originalString размером 80 КБ и sequence из 20 символов, работающий в .NET Core 2.2, с тремя вариациями для каждого. В «случайных» вариантах 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 всегда занимает в 2–3 раза больше, чем CountCharacterWithoutSpan». В этих тестах CountCharacterWithoutSpan неизменно намного медленнее, чем любая из версий на основе ReadOnlySpan<char>. (Однако разница между ними слишком мала, чтобы ее измерить.)
С обоими методами на основе диапазона объем работы, выполняемой при каждом сравнении, является значительным: вы можете увидеть существенные различия между тестами, в которых большинство сравнений строк может завершиться после одного или двух символов, и теми, где это необходимо. сравните каждый персонаж. (Между примерами Random и Match нет значимой разницы - кажется, что разница в стоимости досрочного выхода всех сравнений и досрочного выхода одного из них очень мала. Это не удивительно, поскольку мы в основном смотрим на одно сравнение из 80 000 дорогие, а остальные дешевые.
Совершенно ясно, что версия без диапазона стоит дорого. Убивает его призыв к Substring. Это особенно плохо в тестах, где большинство сравнений терпят неудачу почти сразу: вы выделяете 2000-символьную копию некоторой подстроки originalString, а затем смотрите только на несколько символов.
Обратите внимание, что в случае, когда мы можем выйти под залог раньше, производительность версий на основе диапазона в значительной степени не зависит от длины sequence - примерно 190 мкс во всех случаях. Это то, на что вы надеетесь - в тех случаях, когда мы можем очень рано определить, что совпадения нет, на самом деле не должно иметь значения, какова длина sequence, но в вашей версии без диапазона длина sequence имеет большое значение. даже в этих случаях.
Сколько измерений вы проводите во время тестов? Мне интересно, возможно, вы просто измеряете один прогон, и в этом случае вы на самом деле не измеряете, сколько времени требуется для выполнения кода: вы в основном измеряете, сколько времени требуется компилятору JIT для его компиляции.
Я знаю, что это датировано, но вероятной причиной может быть то, что вам нужно создать промежутки для обеих строк вне цикла.