alexeyfv

Опубликовано

- 2 мин чтения

Самый быстрый способ извлечь подстроку в C#

C# Производительность
img of Самый быстрый способ извлечь подстроку в C#

Сегодня снова поговорим о микробенчмаркинге и производительности в C#. Тема — строки и самый эффективный способ вырезать подстроку из исходной строки.

Бенчмарк

В этом тесте я рассмотрел следующие способы извлечения подстроки:

  1. Метод Substring.
  2. Оператор Range.
  3. Метод Split.
  4. Структура ReadOnlySpan<T>.
  5. Класс Regex.
  6. Метод SkipWhile.

Для тестирования я использовал библиотеку BenchmarkDotNet. Полный код тестов доступен здесь.

Результаты

Как обычно, я запускал бенчмарки на платформах .NET 6 и .NET 7. Разница между ними минимальна.

Время выполнения

Результаты сравнения методов извлечения подстроки в C#

Результаты бенчмарка

Мы видим, что ReadOnlySpan<T>, Substring и оператор Range показывают похожие результаты по скорости. Split, Regex и SkipWhile заметно медленнее: в 2.5, 8.5 и 23.5 раза соответственно.

МетодСреднее, нсПроцент
ReadOnlySpan<T>687.6100
Substring698.5102
Range710.5103
Split1696.3247
Regex5830.4848
SkipWhile16211.72358

Если посмотреть на декомпилированный код, видно, что реализация оператора Range очень похожа на Substring.

   // Оператор Range после декомпиляции
string text = data[num];
int num2 = text.IndexOf(_symbol);
string text2 = text;
int num3 = num2;
list.Add(text2.Substring(num3, text2.Length - num3));
num++;

Разница только в том, что Substring использует меньше локальных переменных:

   // Substring после декомпиляции
string text = data[num];
int startIndex = text.IndexOf(_symbol);
list.Add(text.Substring(startIndex));
num++;

ReadOnlySpan<T> показывает чуть лучшие результаты. Похоже, получение спана памяти и создание строки из него работает быстрее, чем извлечение подстроки через Substring. Вероятно, это связано с дополнительной проверкой границ индексов в внутренней реализации метода Substring.

   // ReadOnlySpan<T> после декомпиляции
string obj = data[num];
int start = obj.IndexOf(_symbol);
ReadOnlySpan<char> value = MemoryExtensions.AsSpan(obj, start);
list.Add(new string(value));
num++;

Split медленнее из-за своей внутренней реализации, и использовать его для получения подстроки — не лучший выбор.

   // Split после декомпиляции
string text = data[num];
list.Add(text.Split(':')[1]);
num++;

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

   // Regex после декомпиляции
string input = data[num];
list.Add(Regex.Match(input, _pattern).Groups[1].Value);
num++;

SkipWhile очень медленный, потому что:

  1. Создаёт новый делегат Func<char, bool>.
  2. Enumerable.SkipWhile вызывает этот делегат для каждого символа строки.
  3. Enumerable.ToArray преобразует IEnumerable<char> в char[].
   // SkipWhile после декомпиляции
string source = data[num];
list.Add(new string(
    Enumerable.ToArray(
        Enumerable.SkipWhile(
            source,
            new Func<char, bool>(<SkipWhile>b__5_0)))));
num++;

Память

По памяти ReadOnlySpan<T>, Substring и Range дают одинаковые результаты. Остальные варианты расходуют больше памяти.

МетодGen0Gen1ВыделеноПроцент
ReadOnlySpan<T>0.39010.00574.79 KB100
Substring0.39010.00574.79 KB100
Range0.39010.00574.79 KB100
Split0.73620.01149.03 KB188
Regex1.91500.030523.5 KB490
SkipWhile2.28880.030528.23 KB589

Вывод

Самыми эффективными способами извлечения подстроки в C# являются ReadOnlySpan<T>, Substring и оператор Range. Мне больше нравится Range, потому что код с ним выглядит чище. Но стоит помнить, что он на 1–3% медленнее, чем ReadOnlySpan<T> и Substring.