안녕하세요 마수리입니다.
오늘은 제가 생각할 때 StringBuilder
의 핵심!!에 대한 설명입니다!
StringBuilder
가 버퍼 크기 이상의 문자열을 추가 하게 되면 버퍼의 크기를 아래와 같은 로직에 의해서 결정하고 새로운 버퍼를 만들고 그 버퍼에 문자열을 복사한다.
int 새로운 버퍼의 크기 = Math.Max(버퍼가 다 차서 못넣었던 나머지 문자열의 크기, Math.Min(총 버퍼의 크기, 8000));
이 코드를 말로 풀어쓰면 아래와 같다. 만약 버퍼가 다 차서 못 넣었던 나머지 문자열의 크기
가 8000을 넘는다면 새로운 버퍼의 크기는 8000 이상이 될 수 있지만 한번에 이렇게 큰 양의 문자열을 입력하는 경우가 드물다고 가정한다면 다음과 같이 생각할 수 있다.
새로운 버퍼의 크기는 지금 것 생성한 모든 버퍼의 크기로 하되 최대값은 8000으로 정한다. (8000을 최대값으로 정한 이유는
LOH
(Large ojbect heap) 때문이고 이 부분에 대해서는 이 글에서는 설명하지 않을 것이다.)
여기서 몇 가지 추측이 가능하다.
1. 버퍼는 1개가 아니다.
새로운 버퍼가 생길 때마다 m_ChunkPrevious
라는 변수로 현재 버퍼를 담고 새로운 버퍼에 연결한다. 이 연결 리스트를 최종적으로 문자열을 뽑는 .ToString()
함수를 이용할 때 모두 방문하며 모든 버퍼의 문자열을 복사해 1개의 string
을 리턴하게 된다. 추후에 이 부분에 대해서 같이 볼 시간이 있을 것이다.
2. 새로운 버퍼를 추가할 때 마다 총 버퍼의 크기는 2배 혹은 그 이상으로 늘어난다.
새로운 버퍼를 만드는 하나의 예를 들면 아래와 같다.
StringBduiler
를 생성한다.StringBuilder
에 이미 다른 14개의 문자를 넣었다고 가정하자. // 이러면 새로운 버퍼의 크기는 이전 버퍼의 크기인 16이 될 것이다.
int 새로운 버퍼의 크기 = Math.Max(버퍼가 다 차서 못넣었던 나머지 문자열의 크기 : 3, Math.Min(총 버퍼의 크기 : 16, 8000));
이러면 결론적으론 16의 크기를 가지고 있던 기존 버퍼와 다시 16의 버퍼 크기를 가지고 있는 새로운 버퍼가 생기게 된다.
결국 최종 버퍼의 크기는 기존 버퍼의 2배가 늘어단 32가 된다는 말이다.
이 공간들은 사용자가 최종적으로 .ToString()
을 한 후 GC
되어야할 공간으로 너무 많은 버퍼를 반복적으로 생성하면 GC
에 부담이 늘어날 수 있게 된다.
정말 글쓴이의 말대로 동작하고 있을까? 상당히 간략화 되어있는 실제 StringBuilder
코드를 볼 예정인데 모든 것을 이해할 필요는 없고 주석을 위주로 이런식으로 돌아가는구나~ 정도로 읽어보면 좋을 것 같다.
// 현재 버퍼보다 입력해야 하는 문자열의 크기가 크다면 아래와 같은 행동을 합니다.
// 실제 코드는 훨씬 복잡합니다.
public unsafe StringBuilder Append()
{
int 현재버퍼에남아있는공간 = 현재버퍼의총길이 - 버퍼에들어있는문자열의길이;
if (현재버퍼에남아있는공간 > 0)
{
// 1. 현재 버퍼에 남은 공간만큼 입력해야하는 문자열의 앞부분을 넣는다.
ThreadSafeCopy();
}
// 2. 그 후 남은 문자열을 새로운 버퍼를 생성하고 그곳에 넣는다.
int 나머지입력해야하는문자열의길이 = 입력해야하는문자열의크기 - 현재버퍼에남아있는공간;
// 새로운 버퍼를 생성한다.
ExpandByABlock(나머지입력해야하는문자열의길이);
// 1번에서 넣고 남은 나머지 문자열을 새로운 버퍼에 입력한다.
ThreadSafeCopy();
return this;
}
위에서 설명드린대로 입력해야할 문자열 중 현재 버퍼에 입력이 가능한 것은 현재 버퍼에 넣고 나머지 부분은 새로운 버퍼를 생성한 후 그 버퍼에 입력하는 것을 볼 수 있다.
ExpandByABlock()
를 볼 수 있는데 이곳에서 위에서 본 로직대로 버퍼의 크기를 구하고 새로운 버퍼를 만들 것이다. 소스코드를 읽어보자
// 실제 코드는 훨씬 복잡합니다.
private void ExpandByABlock(int minBlockCharCount)
{
// 위에서 본 버퍼 크기를 구하는 로직
int newBlockLength = Math.Max(minBlockCharCount, Math.Min(Length, MaxChunkSize));
// 현재 스트링빌더를 m_ChunkPrevious에 저장한다
// 이 변수는 추후에 .ToString() 할 때 사용된다.
m_ChunkPrevious = new StringBuilder(this);
m_ChunkOffset += m_ChunkLength;
m_ChunkLength = 0;
// 새로운 버퍼를 할당한다.
m_ChunkChars = new char[newBlockLength];
}
이 코드 역시 위에서 설명드린바와 같이 버퍼의 크기를 구하는 공식에 의해 버퍼의 크기를 구하고 새로운 버퍼를 생성하고 기존의 버퍼는 m_ChunkPrevious
변수에 할당하고 추후에 .ToString()
을 할 때 사용한다.
StringBuilder
는 버퍼가 가득 찼을 경우 새로운 버퍼를 만들고 기존의 버퍼를 링크드리스트 형태로 가지고 있는다. 새로운 버퍼의 크기는 입력하려는 문자열의 크기에 따라 다르겠지만 한번에 입력하려는 문자가 매우 크지 않은 경우가 많다는 가정하에 보통 링크드리스트로 가지고 있는 모든 버퍼의 총 크기와 같은 크기로 정해질 가능성이 높다. 이렇게 새로운 버퍼가 생성되면 기존과 같은 크기의 버퍼가 1개 더 생기기 때문에 총 버퍼의 크기는 2배가 될 것이다. 오늘은 StringBuilder
의 가장 핵심이 되는 버퍼의 크기를 정하고 새로운 버퍼를 만드는 과정을 보았다. 다음은 마지막 시간으로 최종적으로 .ToString()
을 가지고 최종적인 문자열을 만드는 방법을 보고 마무리 하도록 하겠다!