Опубликовано
- 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
.