Comments 20
Тесты с регулярным выражением надо переделать, у вас "\s+", а должно быть "\s", в остальных тестах я вижу что каждый пробел заменяется на запятую, а не группа пробелов.
Необходимо переделать даже не на "\s", а на регулярку с единственным пробельным символом (space character). Регулярка \s ищет еще кучу всего помимо пробелов (всякие табуляции, переносы строк и вовзраты кареток при включенном многострочном режиме).
А вас не смущают результаты последнего бенчмарка? Разница составляет 4(!) порядка.
Я бы предположил, что компилятор неплохо оптимизирует большинство циклов. Не бежит по всему циклу, а "разворачивает" его в код, сразу выдающий нужный результат.
Было бы интересно взглянуть на IL код этих методов, чтобы понять причины таких радикальных отличий.
Там не только разница на 4 порядка, там еще и 300Мб (!) аллокаций в методе, который все что делает - считает сумму элементов массива.
В бенчмарке явно есть какая-то ошибка
UPD:
Пришлось запустить код самому, что бы увидеть. В последнем бенчмарке, данные в классе с бенчмарком задаются в виде конструкции
private static int[] Items => Enumerable.Range(1, 10_000).ToArray();
Это значит:
При каждом обращении к Items аллоцируется новый массив на 10k элементов
Аллокация массива тестовых данных учитывается в бенчмарке
В итоге, в тестах которые обращаются к Items.Length в цикле, на каждый проход цикла аллоцируется новый массив. Если поправить бенчмарк:
public class NewCyclesBenchmark
{
private readonly int[] _items;
public NewCyclesBenchmark()
{
_items = Enumerable.Range(1, 10_000).ToArray();
}
}
то результаты будут куда менее провокационными
![](https://habrastorage.org/getpro/habr/upload_files/5a3/19a/586/5a319a586cdc89b78807005775d5a700.png)
Там точно ошибка.
В бенчмарке 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, поэтому не меняет. Если сделать не константой, то да, меняет. Заменил и переснял результаты.
Как вы проверяете? Я вижу что строка изменяется и в случае private const string Content
.
https://dotnetfiddle.net/mHRNKR
Результаты тестирования 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);
}
Бенчмаркая строки и циклы: Replace, Split и Substring