.NET에서 문자열을 변경할 수없는 경우 왜 Substring에 O (n) 시간이 걸리나요?


451

.NET에서 문자열을 변경할 수 없다는 것을 감안할 때 왜 string.Substring()O ( substring.Length) 대신 O ( ) 시간 이 걸리 도록 설계되었는지 궁금합니다 O(1).

즉, 트레이드 오프는 무엇입니까?


3
@Mehrdad :이 질문이 마음에 듭니다. .Net에서 주어진 함수의 O ()를 어떻게 결정할 수 있는지 알려주시겠습니까? 명확합니까 아니면 계산해야합니까? 감사합니다
odiseh

1
@ odiseh : 때로는 (이 경우와 같이) 문자열이 복사되는 것이 분명합니다. 그렇지 않은 경우 설명서를 보거나 벤치 마크를 수행하거나 .NET Framework 소스 코드를 확인하여 내용을 알아볼 수 있습니다.
541686

답변:


423

업데이트 : 나는이 질문을 너무 좋아, 방금 블로그에 올렸습니다. 문자열, 불변성 및 지속성을 참조하십시오


짧은 대답은 다음과 같습니다. n이 커지지 않으면 O (n)은 O (1)입니다. 대부분의 사람들은 작은 문자열에서 작은 하위 문자열을 추출하므로 복잡성이 무증상으로 성장하는 방식은 전혀 관련이 없습니다 .

긴 대답은 다음과 같습니다.

인스턴스에 대한 조작이 적은 양 (일반적으로 O (1) 또는 O (lg n))의 복사 또는 새 할당으로 원본의 메모리를 재사용 할 수 있도록 구축 된 불변 데이터 구조를 "지속적"이라고합니다. 불변 데이터 구조. .NET의 문자열은 변경할 수 없습니다. 귀하의 질문은 본질적으로 "왜 지속되지 않는가?"

일반적으로 .NET 프로그램의 문자열에서 수행 되는 작업을 살펴보면 완전히 새로운 문자열을 만드는 것이 전혀 나쁘지 않습니다 . 복잡한 영구 데이터 구조를 구축하는 데 드는 비용과 어려움은 그 자체로 비용을 지불하지 않습니다.

사람들은 일반적으로 "서브 스트링"을 사용하여 약간 긴 문자열에서 짧은 문자열 (예 : 10 ~ 20 자)을 추출합니다. 쉼표로 구분 된 파일에 텍스트 줄이 있고 성인 세 번째 필드를 추출하려고합니다. 줄은 몇 백 자 정도 될 것이고 이름은 수십 개가 될 것입니다. 현대 하드웨어 에서는 50 바이트의 문자열 할당 및 메모리 복사가 매우 빠릅니다 . 기존 문자열의 가운데에 길이를 더한 포인터로 구성된 새로운 데이터 구조를 만드는 것도 놀랍도록 빠릅니다. "충분히 빠름"은 정의상 충분히 빠릅니다.

추출 된 부분 문자열은 일반적으로 크기가 작고 수명이 짧습니다. 가비지 콜렉터가 곧 회수 할 예정이며, 처음에는 힙에 많은 공간을 차지하지 않았습니다. 따라서 대부분의 메모리 재사용을 장려하는 지속적인 전략을 사용하는 것도 승리가 아닙니다. 내부 포인터 처리에 대해 걱정해야하기 때문에 가비지 수집기가 느려집니다.

사람들이 일반적으로 문자열에서 수행하는 하위 문자열 작업이 완전히 다른 경우 지속적인 접근 방식을 사용하는 것이 좋습니다. 사람들이 일반적으로 백만 자의 문자열을 가지고 있으며 수 천 자 범위의 크기를 가진 수천 개의 겹치는 부분 문자열을 추출하고 그 부분 문자열이 힙에서 오랫동안 살았다면 지속적인 부분 문자열을 사용하는 것이 완벽합니다. 접근하다; 그것은 낭비적이고 어리석은 짓입니다. 그러나 대부분의 업무용 프로그래머는 이런 종류의 것들과 같은 모호한 행동을하지 않습니다.. .NET은 Human Genome Project의 요구에 맞춘 플랫폼이 아닙니다. DNA 분석 프로그래머는 이러한 문자열 사용 특성과 관련된 문제를 매일 해결해야합니다. 당신이하지 않을 확률이 좋습니다. 밀접하게 일치하는 자신의 영구적 인 데이터 구조를 구축 않는 극소수의 사람들 자신의 사용 시나리오를.

예를 들어, 우리 팀은 입력 할 때 C # 및 VB 코드를 즉시 분석하는 프로그램을 작성합니다. 이러한 코드 파일 중 일부는 엄청나 므로 하위 문자열을 추출하거나 문자를 삽입 또는 삭제하기 위해 O (n) 문자열 조작을 수행 할 수 없습니다. 우리는 텍스트 버퍼에 대한 편집 내용을 나타 내기 위해 영구적 인 불변 데이터 구조를 많이 만들었습니다.이를 통해 기존 문자열 데이터의 대량 기존 편집시의 어휘 및 구문 분석 을 빠르고 효율적으로 재사용 할 수 있습니다. 이것은 해결하기 어려운 문제였으며 솔루션은 C # 및 VB 코드 편집의 특정 도메인에 맞게 조정되었습니다. 내장 문자열 유형이 우리 에게이 문제를 해결할 것으로 기대하는 것은 비현실적입니다.


47
자바가 어떻게하는지 (또는 적어도 과거 어느 시점에서)했던 것과 대조되는 것은 흥미로울 것입니다. 하위 문자열은 새로운 문자열을 반환하지만 더 큰 문자열과 동일한 char []를 가리 키므로 큰 char [] 부분 문자열이 범위를 벗어날 때까지 더 이상 가비지 수집 할 수 없습니다. 나는 .net의 구현을 훨씬 선호합니다.
Michael Stum

13
나는 이런 종류의 코드를 보았습니다 : string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...또는 다른 버전. 전체 파일을 읽은 다음 다양한 부분을 처리해야합니다. 이러한 종류의 코드는 문자열이 지속적 일 경우 상당히 빠르며 적은 메모리를 필요로합니다. 각 줄을 복사하는 대신 항상 파일 사본을 메모리에 정확히 하나만 저장 한 다음 각 줄의 일부를 처리합니다. 그러나 Eric이 말한 것처럼 일반적인 사용 사례는 아닙니다.
구성자

18
@configurator : 또한 .NET 4에서 File.ReadLines 메서드는 텍스트 파일을 모두 메모리로 먼저 읽을 필요없이 텍스트 파일을 줄 단위로 나눕니다.
Eric Lippert

8
@Michael : Java String는 지속적인 데이터 구조로 구현됩니다 (표준에는 지정되어 있지 않지만 제가 알고있는 모든 구현은이 작업을 수행합니다).
Joachim Sauer

33
짧은 답변 : 데이터의 사본은 원래 문자열의 가비지 수집을 허용합니다 .
Qtax

121

정확하게 때문에 문자열은 불변, .Substring원래 문자열의 적어도 일부의 사본을해야합니다. n 바이트 의 복사본을 만드는 데 O (n) 시간이 걸립니다.

일정한 시간 에 많은 바이트를 복사한다고 생각 하십니까?


편집 : Mehrdad는 문자열을 전혀 복사하지 말고 문자열을 참조하도록 제안합니다.

.Net에서는 누군가가 .SubString(n, n+3)(문자열의 중간에있는 n에 대해) 호출하는 멀티 메가 바이트 문자열을 고려하십시오 .

이제 하나의 참조가 4자를 보유하고 있기 때문에 ENTIRE 문자열을 가비지 수집 할 수 없습니까? 그것은 말도 안되는 공간 낭비처럼 보입니다.

또한 하위 문자열에 대한 참조를 추적하고 (하위 문자열에있을 수도 있음) GC를 피하기 위해 최적의 시간에 복사하려고하면 개념이 악몽이됩니다. 에 복사 .SubString하고 간단한 불변 모델을 유지하는 것이 훨씬 간단하고 안정적 입니다.


편집 : 다음 은 큰 문자열 내에서 하위 문자열에 대한 참조를 유지하는 위험에 대해 잘 읽었습니다 .


5
+1 : 정확히 내 생각. 내부적 memcpy으로는 여전히 O (n) 인 것을 사용합니다.
leppie

7
@abelenky : 전혀 복사하지 않아서? 이미 있습니다. 왜 복사해야합니까?
user541686

2
@Mehrdad : 공연이 끝난 후. 이 경우 안전하지 않습니다. 그런 다음 char*부분 문자열을 얻을 수 있습니다 .
leppie

9
@Mehrdad - 당신은이 호출, 너무 많은이 기대 될 수 StringBuilder에 , 그리고 그것의 좋은 건물 문자열. 그것은 StringMultiPurposeManipulator라는 아니에요
MattDavey

3
@SamuelNeff, @Mehrdad : .NET의 문자열은 종료 되지 않습니다 NULL . Lippert의 post에 설명 된대로 처음 4 바이트에는 문자열 길이가 포함됩니다. 이것이 Skeet이 지적한 것처럼 \0문자 를 포함 할 수있는 이유 입니다.
Elideb

33

Java (.NET과 반대)는 두 가지 방법을 제공 Substring()하므로 참조 만 유지할 것인지 전체 하위 문자열을 새 메모리 위치에 복사 할 것인지 고려할 수 있습니다.

단순 .substring(...)은 내부적으로 사용 된 char배열을 원본 String 객체와 공유 한 다음 new String(...)필요한 경우 새 배열에 복사 할 수 있습니다 (원래의 가비지 수집을 방해하지 않도록).

이런 종류의 유연성은 개발자에게 가장 적합한 옵션이라고 생각합니다.


50
당신은 그것을 "유연성"이라고 부른다. 문자열 중간에서 4자를 가져 오려면 "다음 버전에서만 발명 된 문자 포함"에서 호출됩니다. "
Nir

3
downvote retracted ... 좀 더 신중하게 코드를 탐색 한 후에는 적어도 openjdk 버전에서 Java의 하위 문자열이 공유 배열을 참조하는 것처럼 보입니다. 그리고 새로운 문자열을 원한다면 그렇게 할 수있는 방법이 있습니다.
Don Roby

11
@Nir : 나는 이것을 "상태 유지 바이어스"라고 부릅니다. Java 방식은 위험으로 가득 차 있으며 .Net 방식은 현명한 선택입니다. Java 프로그래머에게는 그 반대입니다.
Michael Borgwardt

7
.NET을 강력하게 선호하지만 Java가 제대로 한 것처럼 들립니다. 개발자가 자신의 문자열 유형을 롤링하지 않고 진정으로 O (1) 하위 문자열 메소드에 액세스 할 수 있도록 하는 것이 유용합니다. 이는 다른 모든 라이브러리와의 상호 운용성을 방해하며 기본 제공 솔루션만큼 효율적이지 않습니다. ). Java의 솔루션은 아마도 비효율적 일 것입니다 (원래 문자열과 하위 문자열에 대해 두 개 이상의 힙 객체 필요). 슬라이스를 지원하는 언어는 두 번째 객체를 스택의 포인터 쌍으로 효과적으로 대체합니다.
Qwertie 2016 년

10
JDK 7u6부터는 더 이상 사실이 아닙니다. 이제 Java는 항상 각 문자열 내용을 복사합니다 .substring(...).
Xaerxess

12

Java는 더 큰 문자열을 참조하는 데 사용되었지만 다음과 같습니다.

Java는 메모리 누수를 피하기 위해 동작을 복사변경했습니다 .

그래도 향상 될 수 있다고 생각합니다. 복사를 조건부로 복사하는 것이 어떻습니까?

부분 문자열이 부모 크기의 절반 이상이면 부모를 참조 할 수 있습니다. 그렇지 않으면 사본을 만들 수 있습니다. 이렇게하면 많은 메모리 누수가 발생하지 않으면서도 상당한 이점이 있습니다.


항상 복사하면 내부 어레이를 제거 할 수 있습니다. 짧은 문자열의 일반적인 경우에 메모리를 절약하면서 힙 할당 수를 반으로 줄입니다. 또한 각 문자 액세스에 대한 추가 간접 지시를 건너 뛸 필요가 없습니다.
코드 InChaos

2
이것에서 취해야 할 중요한 점은 Java가 실제로 동일한 기반 char[](시작 및 끝에 대한 다른 포인터 사용)에서 새로운 생성으로 변경되었다는 것 String입니다. 이것은 비용-편익 분석이 새로운 창조를위한 선호를 보여 주어야한다는 것을 분명히 보여줍니다 String.
계통 발생

2

여기에서 "괄호 문제"를 해결 한 답변은 없습니다. 즉, .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*에서 a에 fixed지정된 경우 포인터는 0x48을 가리 킵니다.)

이 구조를 사용하면 문자열 길이를 빠르게 조회 할 수 있으며 (많은 컨텍스트에서 유용) 널 종료 문자열을 예상하는 Win32 (또는 기타) API에 대한 P / Invoke로 포인터를 전달할 수 있습니다.

당신이 할 때 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>때문에 동일한 차이-단으로, string존재한다.

이전 답변에 대한 답변 / 의견 중 일부는 가비지 수집기가 5 백만 자의 문자열을 유지하면서 5 자에 대해 계속 이야기하는 것이 낭비 적이라고 말했습니다. 이것이 바로 ReadOnlySpan<char>접근 방식으로 얻을 수있는 행동 입니다. 짧은 계산을 수행하는 경우 ReadOnlySpan 접근 방식이 더 좋습니다. 잠시 동안 유지해야하고 원래 문자열의 작은 비율 만 유지하려는 경우 적절한 하위 문자열 (과잉 데이터를 제거하기 위해)을 수행하는 것이 좋습니다. 어딘가에 전환점이 있지만 특정 용도에 따라 다릅니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.