Jeśli ciągi są niezmienne in.NET, to dlaczego substrat zajmuje O (n) czas?

Biorąc pod uwagę, że ciągi są niezmienne w. NET, zastanawiam się, dlaczego zostały zaprojektowane tak, że string.Substring() zajmuje O(substring.Length) czas, zamiast O(1)?

Czyli jakie były kompromisy, jeśli w ogóle?

Author: Soner Gönül, 2011-07-19

5 answers

UPDATE: tak bardzo spodobało mi się to pytanie, właśnie je napisałem na blogu. Zobacz ciągi, niezmienność i trwałość


Krótka odpowiedź brzmi: O(n) jest O (1), jeśli n nie rośnie. większość ludzi wyodrębnia małe podciągi z małych ciągów, więc jak złożoność rośnie asymptotycznie jest zupełnie nieistotne.

Długa odpowiedź brzmi:

Niezmienna struktura danych zbudowana w taki sposób, że operacje na instancji pozwalają na ponowne wykorzystanie pamięci oryginału z niewielką ilością(zazwyczaj O(1) lub O (lg n)) kopiowania lub nowej alokacji nazywa się" trwałą " niezmienną strukturę danych. Ciągi w. Net są niezmienne; twoje pytanie brzmi zasadniczo "dlaczego nie są trwałe"?

Ponieważ kiedy patrzysz na operacje zazwyczaj wykonywane na łańcuchach w programach.NET, to pod każdym względem nie jest wcale gorzej aby po prostu zrobić zupełnie nowy łańcuch. Koszt i trudność budowy kompleksu struktura danych sama się nie opłaca.

Ludzie zazwyczaj używają" podłańcucha", aby wyodrębnić krótki łańcuch-powiedzmy, dziesięć lub dwadzieścia znaków-z nieco dłuższego łańcucha-może kilkaset znaków. Masz wiersz tekstu w pliku oddzielonym przecinkami i chcesz wyodrębnić trzecie pole, które jest nazwiskiem. Linia będzie mieć może kilkaset znaków, nazwa będzie kilkadziesiąt. Alokacja łańcuchów i kopiowanie pamięci pięćdziesięciu bajtów jest zadziwiająco szybki na nowoczesnym sprzęcie. To, że tworzenie nowej struktury danych, która składa się ze wskaźnika do środka istniejącego ciągu znaków plus długość jest również zadziwiająco szybkie jest nieistotne; "wystarczająco szybko" jest z definicji wystarczająco szybko.

Wydobywane podciągi są zazwyczaj małe i krótkie w czasie eksploatacji; garbage collector zamierza je wkrótce odzyskać i nie zajmowały dużo miejsca na stercie w pierwszej kolejności. Tak więc stosowanie wytrwałej strategii, która zachęca do ponownego wykorzystania większość pamięci również nie jest wygrana; wszystko, co zrobiłeś, to sprawiłeś, że Twój garbage collector stał się wolniejszy, ponieważ teraz musi martwić się o obsługę wskaźników wewnętrznych.

Jeśli operacje na podciągach zazwyczaj wykonywane na łańcuchach były zupełnie inne, wtedy byłoby sensowne stosowanie metody persistent. Gdyby ludzie zwykle mieli ciągi milionów znaków i wydobywali tysiące nakładających się podciągów o rozmiarach w zakresie stu tysięcy znaków, a te podciągi żył długo na stosie, wtedy byłoby to doskonałe sensu, aby przejść z uporczywym podejściem podciągu; byłoby marnotrawstwem i Głupotą, aby nie. Ale większość programistów nie robi nic, nawet niejasno, jak tego rodzaju rzeczy. . NET nie jest platformą, która jest dostosowana do potrzeb projektu ludzkiego genomu; Programiści analizy DNA muszą rozwiązywać problemy z tymi charakterystykami użycia ciągów każdego dnia; szanse są dobre, że nie. Nieliczni, którzy budują swoje własne trwałe struktury danych, które ściśle odpowiadają ich scenariuszom użycia.

Na przykład, mój zespół pisze programy, które wykonują w locie analizę kodu C# i VB podczas jego wpisywania. Niektóre z tych plików kodu są ogromne i dlatego nie możemy wykonywać manipulacji łańcuchami znaków O(n), aby wyodrębnić podłańcuchy lub wstawić lub usunąć znaki. Zbudowaliśmy kilka trwałych, niezmiennych struktur danych do reprezentowania edycji w buforze tekstowym, które pozwalają nam szybko i efektywnie ponownie wykorzystać większość istniejących danych ciągów i istniejących analiz leksykalnych i składniowych po typowej edycji. Był to trudny problem do rozwiązania, a jego rozwiązanie było wąsko dostosowane do konkretnej domeny edycji kodu C# i VB. Nierealistyczne byłoby oczekiwanie, że wbudowany typ string rozwiąże ten problem za nas.

 403
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-08 09:34:06

Dokładnie Ponieważ ciągi znaków są niezmienne, .Substring musi wykonać kopię co najmniej części oryginalnego ciągu znaków. Wykonanie kopii N bajtów powinno zająć O (n) czas.

Jak myślisz, jak skopiujesz kilka bajtów w stałym czasie?


EDIT: Mehrdad sugeruje, aby w ogóle nie kopiować łańcucha, ale zachować odniesienie do jego fragmentu.

Rozważmy w. Net, wielomegabajtowy ciąg znaków, na który ktoś wywołuje .SubString(n, n+3) (dla dowolnego n w środku Sznurka).

Teraz, cały łańcuch nie może być zbierany śmieci tylko dlatego, że jedno odniesienie jest trzymane na 4 znaki? To wygląda na niedorzeczne marnowanie miejsca.

Ponadto, śledzenie odwołań do podłańcuchów (które mogą być nawet wewnątrz podłańcuchów) i próba skopiowania w optymalnym czasie, aby uniknąć pokonania GC (jak opisano powyżej), sprawia, że koncepcja jest koszmarna. O wiele prostsze i bardziej niezawodne jest kopiowanie na .SubString i utrzymywanie prostego niezmiennego model.


EDIT: Oto dobra mała lektura o niebezpieczeństwie utrzymywania odniesień do podłańcuchów w większych łańcuchach.

 115
Author: abelenky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-20 16:00:45

Java (w przeciwieństwie do. Net) zapewnia dwa sposoby działania Substring(), możesz rozważyć, czy chcesz zachować tylko odniesienie, czy skopiować cały podłańcuch do nowej lokalizacji pamięci.

Prosta .substring(...) dzieli wewnętrznie używaną tablicę char z oryginalnym obiektem String, który następnie za pomocą {[3] } można skopiować do nowej tablicy, w razie potrzeby (aby uniknąć utrudnień w zbieraniu śmieci oryginalnej tablicy).

Myślę, że taka elastyczność jest najlepszą opcją dla dewelopera.
 32
Author: sll,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-23 23:06:54

Java używane do odwoływania się do większych ciągów, ale:

Java zmieniła swoje zachowanie na jak również, aby uniknąć wycieku pamięci.

Wydaje mi się jednak, że można to poprawić: dlaczego po prostu nie zrobić kopiowania warunkowo?

Jeśli podłańcuch jest co najmniej o połowę mniejszy od rodzica, można odwołać się do niego. W przeciwnym razie można zrobić kopię. Pozwala to uniknąć wycieku dużej ilości pamięci, jednocześnie zapewniając znaczne korzyści.

 10
Author: Mehrdad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-03 19:21:17

Żadna z odpowiedzi nie rozwiązała problemu bracketingu, który polega na tym, że ciągi znaków w.Net są reprezentowane jako kombinacja BSTR (długość przechowywana w pamięci "przed" wskaźnikiem) i CStr (ciąg kończy się na '\0').

Łańcuch "Hello there" jest więc reprezentowany jako

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

(Jeśli przypisany do char* w instrukcji fixed - wskaźnik wskazywałby na 0x48.)

Ta struktura pozwala na szybkie wyszukiwanie długości łańcucha (przydatne w wielu jest to możliwe dzięki temu, że nie jest to możliwe, ponieważ nie jest to możliwe, ponieważ nie jest to możliwe.

Kiedy robisz Substring(0, 5) zasada "oh, ale obiecałem, że będzie znak null po ostatnim znaku" mówi, że musisz zrobić kopię. Nawet jeśli masz podłańcuch na końcu, to nie ma miejsca, aby umieścić długość bez uszkodzenia innych zmiennych.


Czasami jednak naprawdę chcesz porozmawiać o " środku Sznurka", i niekoniecznie zależy ci na zachowaniu P/Invoke. Ostatnio dodana struktura ReadOnlySpan<T> może zostać użyta do uzyskania podłańcucha bez kopiowania:

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

ReadOnlySpan<char> "substring" przechowuje długość niezależnie i nie gwarantuje, że po końcu wartości znajduje się "\0". Może być używany na wiele sposobów "jak ciąg", ale nie jest "ciągiem", ponieważ nie ma ani właściwości BStr, ani CSTR (a tym bardziej obu). Jeśli nigdy (bezpośrednio) P / wywołasz to nie ma zbyt wiele różnica (chyba że API, które chcesz wywołać nie ma przeciążenia ReadOnlySpan<char>).

ReadOnlySpan<char> nie może być używany jako pole typu odniesienia, więc jest też ReadOnlyMemory<char> (s.AsMemory(0, 5)), co jest pośrednim sposobem posiadania ReadOnlySpan<char>, więc istnieją te same różnice-od-string.

Niektóre z odpowiedzi / komentarzy do poprzednich odpowiedzi mówiły o marnotrawstwie, aby garbage collector musiał trzymać ciąg milionów znaków wokół, podczas gdy nadal mówisz o 5 znakach. To jest dokładnie takie zachowanie można uzyskać przy podejściu ReadOnlySpan<char>. Jeśli robisz tylko krótkie obliczenia, podejście ReadOnlySpan jest prawdopodobnie lepsze. Jeśli musisz utrzymywać go przez jakiś czas i zachowasz tylko niewielki procent oryginalnego ciągu, prawdopodobnie lepiej będzie wykonać odpowiedni podłańcuch (aby odciąć nadmiar danych). Gdzieś pośrodku znajduje się punkt przejściowy, ale zależy to od konkretnego zastosowania.

 1
Author: bartonjs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-16 16:21:20