Опубликовано
- 2 мин чтения
Самый быстрый способ извлечь подстроку в C#
Сегодня снова поговорим о микробенчмаркинге и производительности в C#. Тема — строки и самый эффективный способ вырезать подстроку из исходной строки.
Бенчмарк
В этом тесте я рассмотрел следующие способы извлечения подстроки:
- Метод
Substring. - Оператор
Range. - Метод
Split. - Структура
ReadOnlySpan<T>. - Класс
Regex. - Метод
SkipWhile.
Для тестирования я использовал библиотеку BenchmarkDotNet. Полный код тестов доступен здесь.
Результаты
Как обычно, я запускал бенчмарки на платформах .NET 6 и .NET 7. Разница между ними минимальна.
Время выполнения
Результаты бенчмарка
Мы видим, что ReadOnlySpan<T>, Substring и оператор Range показывают похожие результаты по скорости. Split, Regex и SkipWhile заметно медленнее: в 2.5, 8.5 и 23.5 раза соответственно.
| Метод | Среднее, нс | Процент |
|---|---|---|
ReadOnlySpan<T> | 687.6 | 100 |
Substring | 698.5 | 102 |
Range | 710.5 | 103 |
Split | 1696.3 | 247 |
Regex | 5830.4 | 848 |
SkipWhile | 16211.7 | 2358 |
Если посмотреть на декомпилированный код, видно, что реализация оператора 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 очень медленный, потому что:
- Создаёт новый делегат
Func<char, bool>. Enumerable.SkipWhileвызывает этот делегат для каждого символа строки.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 дают одинаковые результаты. Остальные варианты расходуют больше памяти.
| Метод | Gen0 | Gen1 | Выделено | Процент |
|---|---|---|---|---|
ReadOnlySpan<T> | 0.3901 | 0.0057 | 4.79 KB | 100 |
Substring | 0.3901 | 0.0057 | 4.79 KB | 100 |
Range | 0.3901 | 0.0057 | 4.79 KB | 100 |
Split | 0.7362 | 0.0114 | 9.03 KB | 188 |
Regex | 1.9150 | 0.0305 | 23.5 KB | 490 |
SkipWhile | 2.2888 | 0.0305 | 28.23 KB | 589 |
Вывод
Самыми эффективными способами извлечения подстроки в C# являются ReadOnlySpan<T>, Substring и оператор Range. Мне больше нравится Range, потому что код с ним выглядит чище. Но стоит помнить, что он на 1–3% медленнее, чем ReadOnlySpan<T> и Substring.