Почему мой ReadOnlySpan <char> медленнее моей строки?

Я играю с 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 12.08.2018 21:57
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
1
622
1

Ответы 1

Вопреки тому, что вы сказали в своем вопросе, версия на основе диапазона на самом деле намного быстрее, чем версия на основе диапазона.

Согласно предложению 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 для его компиляции.

Другие вопросы по теме