Если строки являются неизменяемыми в .NET, то почему Substring занимает O (n) времени?

446

Учитывая, что строки являются неизменными в .NET, мне интересно, почему они были сконструированы таким образом, что string.Substring() занимает время O (substring.Length), а не O(1)?

то есть. какие были компромиссы, если они есть?

  • 2
    что такое? длина строки возможно?
  • 0
    @ Мухаммед: Да, извините, исправлено ...
Показать ещё 2 комментария
Теги:
string
substring
time-complexity

5 ответов

420
Лучший ответ

ОБНОВЛЕНИЕ: Мне очень понравился этот вопрос, я просто написал его в блоге. См. Строки, неизменность и настойчивость


Короткий ответ: O (n) - O (1), если n не растет. Большинство людей извлекают крошечные подстроки из крошечных строк, поэтому, как сложность растет асимптотически, совершенно не имеет значения.

Долгий ответ:

Непрерывная структура данных, построенная таким образом, что операции над экземпляром позволяют повторно использовать память оригинала только с небольшой суммой (как правило, O (1) или O (lg n)) копирования или нового распределения "постоянная" неизменяемая структура данных. Строки в .NET неизменяемы; ваш вопрос по существу "почему они не настойчивы"?

Потому что, когда вы смотрите на операции, которые обычно выполняются в строках в .NET-программах, все равно трудно сделать совсем новую строку. Расходы и сложность построения сложной постоянной структуры данных не оплачиваются сами.

Люди обычно используют "подстроку" для извлечения короткой строки - скажем, десять или двадцать символов - из более длинной строки - может быть, несколько сотен символов. У вас есть строка текста в файле, разделенном запятыми, и вы хотите извлечь третье поле, которое является фамилией. Длина линии может составлять пару сотен символов, имя будет несколько десятков. Распределение строк и копирование памяти в пятьдесят байт поразительно быстро на современном оборудовании. То, что создание новой структуры данных, состоящей из указателя на середину существующей строки плюс длина, также поразительно быстро, не имеет значения; "достаточно быстро" по определению достаточно быстро.

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

Если операции подстроки, которые обычно выполнялись на строках, были совершенно разными, тогда было бы целесообразно использовать постоянный подход. Если люди обычно имели миллионные строки и извлекали тысячи перекрывающихся подстрок с размерами в диапазоне сотен тысяч символов, и эти подстроки долгое время находились в куче, тогда было бы разумно идти с постоянной подстрокой подход; было бы расточительно и глупо не делать этого. Но большинство бизнес-программистов ничего не делают, даже смутно, как подобные вещи..NET не является платформой, которая предназначена для нужд Проекта генома человека; Программисты анализа ДНК должны ежедневно решать проблемы с этими характеристиками использования строк; шансы хорошие, что вы этого не делаете. Те немногие, кто создает собственные постоянные структуры данных, которые точно соответствуют их сценариям использования.

Например, моя команда пишет программы, которые выполняют "на лету" анализ кода С# и VB при вводе. Некоторые из этих файлов кода огромны, и поэтому мы не можем выполнять строчную манипуляцию O (n) для извлечения подстрок или вставки или удаления символов. Мы создали кучу постоянных неизменных структур данных для представления редактирований в текстовый буфер, которые позволяют нам быстро и эффективно повторно использовать основную часть существующих строковых данных и существующих лексических и синтаксических анализов при типичном редактировании. Это была трудная задача для решения, и ее решение было узко адаптировано к конкретной области редактирования кода С# и VB. Было бы нереалистично ожидать, что встроенный тип строки разрешит эту проблему для нас.

  • 47
    Было бы интересно противопоставить то, как Java это делает (или, по крайней мере, когда-то в прошлом): Substring возвращает новую строку, но указывает на тот же символ [], что и большая строка - это означает, что чем больше символ [] больше нельзя собирать мусор, пока подстрока не выйдет из области видимости. Я предпочитаю реализацию .net на сегодняшний день.
  • 13
    Я видел такой код довольно немного: string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ... или другие его версии. Я имею в виду прочитать весь файл, а затем обработать различные части. Код такого рода будет значительно быстрее и потребует меньше памяти, если строка будет постоянной; у вас всегда будет ровно одна копия файла в памяти вместо того, чтобы копировать каждую строку, а затем части каждой строки в процессе ее обработки. Однако, как сказал Эрик, это не типичный вариант использования.
Показать ещё 22 комментария
109

Именно потому, что строки неизменяемы, .Substring должен сделать копию, по крайней мере, части исходной строки. Создание копии из n байтов должно занимать время O (n).

Как вы думаете, вы скопировали кучу байтов в постоянное время?


РЕДАКТИРОВКА: Мехрдад предлагает не копировать строку вообще, а сохранять ссылку на ее часть.

Рассмотрим в .Net строку с несколькими мегабайтами, на которую кто-то вызывает .SubString(n, n+3) (для любого n в середине строки).

Теперь, строка ENTIRE не может быть собрана мусором только потому, что одна ссылка удерживает до 4 символов? Это кажется смешной тратой пространства.

Кроме того, отслеживание ссылок на подстроки (которые могут быть даже внутри подстрок) и попытка скопировать в оптимальные моменты времени, чтобы избежать поражения GC (как описано выше), делает концепцию кошмаром. Это намного проще и надежнее копировать на .Substring и поддерживать неизменную неизменную модель.


EDIT: Здесь хорошо читать об опасности хранения ссылок на подстроки в больших строках.

  • 5
    +1: именно мои мысли. Внутренне это, вероятно, использует memcpy который все еще O (n).
  • 7
    @abelenky: Я думаю, может быть, не копируя это вообще? Это уже там, почему вы должны скопировать его?
Показать ещё 22 комментария
31

Java (в отличие от .NET) предоставляет два способа выполнения Substring(), вы можете рассмотреть, хотите ли вы сохранить только ссылку или скопировать всю подстроку в новую ячейку памяти.

Простой .substring(...) делится внутренне используемым массивом char с исходным объектом String, который вы затем с помощью new String(...) можете при необходимости скопировать в новый массив (чтобы избежать затруднения сборки мусора исходного).

Я думаю, что такая гибкость - лучший вариант для разработчика.

  • 0
    Что вы подразумеваете под «Первоначально» здесь? Это было удалено?
  • 0
    @Henk Holterman: извините за путаницу, я считаю, что это из-за моего чистого английского, извиняюсь
Показать ещё 8 комментариев
10

Java используется для ссылки на большие строки, но:

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

Я чувствую, что он может быть улучшен, хотя: почему бы просто не копировать условно?

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

  • 0
    Всегда копирование позволяет удалить внутренний массив. Уменьшает вдвое количество выделений кучи, экономя память в общем случае коротких строк. Это также означает, что вам не нужно перепрыгивать через дополнительную косвенность для каждого доступа персонажа.
  • 2
    Я думаю, что важная вещь, которую можно извлечь из этого, заключается в том, что Java фактически изменилась с использования одного и того же базового char[] (с разными указателями на начало и конец) на создание новой String . Это ясно показывает, что анализ затрат и выгод должен показывать предпочтение созданию новой String .
0

Ни один из ответов здесь не упоминал "проблему брекетинга", то есть строки в.NET представляются как комбинация BStr (длина, хранящаяся в памяти) до "указателя" и CStr (строка заканчивается на '\ 0').

Строка "Hello there" представляется таким образом как

0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00

(если назначено char* в fixed -statement, указатель указывает на 0x48.)

Эта структура позволяет быстро найти длину строки (полезную во многих контекстах) и позволяет передавать указатель в API P/Invoke to Win32 (или другие), которые ожидают строку с завершающим нулем.

Когда вы выполняете Substring(0, 5) "о, но я обещал, что после последнего символа будет символ нулевого символа", вы должны сделать копию. Даже если у вас есть подстрока в конце, тогда не будет места, чтобы положить длину без искажения других переменных.


Иногда, однако, вы действительно хотите говорить о "середине строки", и вам не обязательно заботиться о поведении P/Invoke. Недавно добавленную структуру ReadOnlySpan<T> можно использовать для получения подстроки без копии:

string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);

ReadOnlySpan<char> "сохраняет длину независимо, и она не гарантирует, что после окончания значения будет"\0 ". Он может использоваться многими способами "как строка", но это не "строка", поскольку он не имеет ни характеристик BStr, ни CStr (тем более их обоих). Если вы никогда не (напрямую) P/Invoke, то разница между ними невелика (если только API, который вы хотите вызвать, не имеет перегрузки ReadOnlySpan<char>).

ReadOnlySpan<char> не может использоваться как поле ссылочного типа, поэтому также имеется ReadOnlyMemory<char> (s.AsMemory(0, 5)), что является косвенным способом иметь ReadOnlySpan<char>, поэтому те же различия -from- string существует.

Некоторые из ответов/комментариев по предыдущим ответам говорили о том, что это расточительно, если сборщик мусора должен содержать строку в миллион символов, в то время как вы продолжаете говорить о 5 символах. Именно такое поведение вы можете получить с помощью метода ReadOnlySpan<char>. Если вы просто делаете короткие вычисления, подход ReadOnlySpan, вероятно, лучше. Если вам нужно некоторое время упорствовать, и вы будете удерживать только небольшой процент исходной строки, то правильная подстрока (чтобы обрезать лишние данные), вероятно, лучше. Там точка перехода где-то посередине, но это зависит от вашего конкретного использования.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню