Pull to refresh

Comments 20

Тесты с регулярным выражением надо переделать, у вас "\s+", а должно быть "\s", в остальных тестах я вижу что каждый пробел заменяется на запятую, а не группа пробелов.

UFO just landed and posted this here

Спасибо за идею, дополнил и переделал тесты

Подобный способ тестируется, но без статической переменной, что несколько хуже из-за времени на поиск в кэше...

Необходимо переделать даже не на "\s", а на регулярку с единственным пробельным символом (space character). Регулярка \s ищет еще кучу всего помимо пробелов (всякие табуляции, переносы строк и вовзраты кареток при включенном многострочном режиме).

А вас не смущают результаты последнего бенчмарка? Разница составляет 4(!) порядка.

Я бы предположил, что компилятор неплохо оптимизирует большинство циклов. Не бежит по всему циклу, а "разворачивает" его в код, сразу выдающий нужный результат.

Было бы интересно взглянуть на IL код этих методов, чтобы понять причины таких радикальных отличий.

Там не только разница на 4 порядка, там еще и 300Мб (!) аллокаций в методе, который все что делает - считает сумму элементов массива.

В бенчмарке явно есть какая-то ошибка

UPD:

Пришлось запустить код самому, что бы увидеть. В последнем бенчмарке, данные в классе с бенчмарком задаются в виде конструкции

private static int[] Items => Enumerable.Range(1, 10_000).ToArray();

Это значит:

  1. При каждом обращении к Items аллоцируется новый массив на 10k элементов

  2. Аллокация массива тестовых данных учитывается в бенчмарке

В итоге, в тестах которые обращаются к Items.Length в цикле, на каждый проход цикла аллоцируется новый массив. Если поправить бенчмарк:

public class NewCyclesBenchmark
{
    private readonly int[] _items;

    public NewCyclesBenchmark()
    {
        _items = Enumerable.Range(1, 10_000).ToArray();
    }
}

то результаты будут куда менее провокационными

Там точно ошибка.

В бенчмарке For есть надпись i < Items.Length. Items в данном бенчмарке это статическое свойство, которое вычисляется при обращении к нему. То есть при каждой итерации по циклу мы каждый раз дергаем свойство Items где каждый раз заново создается массив у которого берется Length.

Отсюда такая бешенная аллокация.

Отмечу ещё, что автор статьи не бежит по массиву, а просто инкрементирует i и приплюсовывает результат к sum. То есть он не обращается к содержимому массива вообще. В деле поиска подстроки это поможет вряд ли, так как строку для разделения всё-таки надо читать.

Тут да, недоглядел. Переделал и переснял результаты

Так как Items к которому обращаются в условии цикле является вычисляемым, то оно вычисляется в каждой итерации.

Это исправить легко, необходимо до цикла один раз вычислить длинну, как это было сделано с новым while.

Нельзя в одном случае сравнивать с пробелом, а в другом использовать IsWhiteSpace, потому что whitespace это целое множество символов.

А зачем мы в этом способе выделяем новую строку (2), если мы уже поменяли исходную (1)?


    [Benchmark]
    public string Marshal_Span()
    {
        Span<char> chars = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(Content.AsSpan()), Content.Length);
        for (int i = 0; i < chars.Length; i++)
        {
            if (chars[i] == ' ')
            {
                chars[i] = ',';
            }
        }

        return Content; (1)
        return chars.ToString(); (2)
    }

|                                Method |        Mean |     Error |    StdDev | Allocated |
|-------------------------------------- |------------:|----------:|----------:|----------:|
|                               Replace |    55.15 ns |  0.110 ns |  0.102 ns |     824 B |
|                                  Join |   877.49 ns |  2.387 ns |  2.233 ns |    3480 B |
|                            Base_Regex | 2,107.93 ns |  2.620 ns |  2.323 ns |     936 B |
|                       Generated_Regex | 2,167.31 ns | 23.824 ns | 19.894 ns |     824 B |
|                     Сonstructor_Regex | 2,175.78 ns | 17.405 ns | 14.534 ns |     824 B |
|                        Char_NewString |   347.75 ns |  0.762 ns |  0.713 ns |    1648 B |
|                Char_NewString_FastFor |   345.49 ns |  0.764 ns |  0.677 ns |    1648 B |
|                        Span_NewString |   346.33 ns |  1.008 ns |  0.943 ns |    1648 B |
|                           Span_Concat | 1,503.21 ns |  1.903 ns |  1.687 ns |    2504 B |
|                           Char_Concat | 1,453.93 ns |  3.316 ns |  2.940 ns |    1680 B |
|         Unsafe_ReadOnlySpan_NewString |   297.68 ns |  0.470 ns |  0.416 ns |     824 B |
| Unsafe_ReadOnlySpan_NewString_Foreach |   308.40 ns |  1.223 ns |  1.144 ns |     824 B |
|                         Unsafe_String |   257.23 ns |  0.308 ns |  0.288 ns |         - |
|                 Unsafe_String_Foreach |   292.52 ns |  0.715 ns |  0.634 ns |         - |
|                          Marshal_Span |   257.43 ns |  0.517 ns |  0.484 ns |         - |
|                 Marshal_Span_ToString |   302.45 ns |  0.891 ns |  0.790 ns |     824 B |
|                Marshal_Span_NewString |   297.55 ns |  0.448 ns |  0.419 ns |     824 B |
|                         String_Create |   300.35 ns |  0.430 ns |  0.359 ns |     824 B |

Исходная не меняется, поэтому нужно работать с результатом в виде chars, что можно проверить вызвав метод просто так.

В данных тестах у меня private const string Content, поэтому не меняет. Если сделать не константой, то да, меняет. Заменил и переснял результаты.

Действительно, не то смотрел. Да, все работает и так и так

Результаты тестирования unsafe методов абсолютно неверны, потому что сами методы написаны неправильно. Закрепление объекта внутри цикла так себе идея, fixed нужно вынести на уровень выше.

public unsafe string Unsafe_ReadOnlySpan_NewString()
{
    ReadOnlySpan<char> chars = Content.AsSpan();
    
    fixed (char* baseChar = chars)
        for (int i = 0; i < chars.Length; i++)
        {
            if (baseChar[i] == ' ') baseChar[i] = ',';
        }
    return new string(chars);
}

Спасибо за комментарий. Поправил и обновил результаты тестов

Sign up to leave a comment.

Articles