3. String과 StringBuilder 비교

  string은 프로그램을 짜면서 가장 빈번하게 사용되는 자료형의 하나이다. string을 쓰는데 있어서 흔히들 string 연산이 많을 경우 string 타입 대신에 StringBuilder 객체를 쓰라는 얘기를 들어 보았을 것이다. 여기에서는 string과 StringBuilder의 차이에 대해 다루며 각각을 어떻게 이용하는 것이 효율적인지에 대해 다루어보도록 하겠다.

  먼저 아래의 간단한 프로그램을 보도록 하자.

class Program

{

    static void Main(string[] args)

    {

        string str1 = ""

        StringBuilder strBuilder = new StringBuilder();

 

        // string object

        Console.WriteLine("\nString Object\n");

        for (int i = 0; i < 17; i++)

        {

            str1 = str1 + "A"

            Console.WriteLine(GC.GetTotalMemory(true).ToString());

        }

 

        // StringBuilder object

        Console.WriteLine("\nStringBuilder Object\n");

        for (int i = 0; i < 17; i++)

        {

            strBuilder.Append("A");

            Console.WriteLine(GC.GetTotalMemory(true).ToString());

        }

    }

}


  위의 예제 소스는 간단히 프로그램의 메모리 사용량을 비교해보기 위해 만들어본 소스이다. 단순히 증가되는 프로그램 영역의 메모리를 카운트하는 용도로 GetTotalMemory 라는 메서드를 이용해서 비교해 보았다. 이제 결과를 보기로 하자.





그림. 실행결과


  string

  실행결과 그림에서 보이는 바와 같이 string 으로 선언된 str1 변수의 경우 17번 반복하는 동안 연속적인 메모리 증가를 보인다. 실제 string의 내부적인 구현은 char 타입의 연속적인 열의 형태로 구성되는데, 선언시 초기화되는 데이터의 값으로 string 타입 변수의 크기가 결정된다고 보면 된다. 위의 소스와 같이 “+”와 같은 결합 연산자를 이용하여 경우 데이터의 크기가 증가하므로 할당되는 메모리 역시 증가되어야 한다.


  이제 코드를 상세히 보도록 하자. 위의 소스에서 다룬 스트링 연산에서,

 str1 = str1+"A";


  부분을 디스어셈블러를 통해 추적해 보면 Concat(string, string)를 호출하는 것을 볼수 있다.

  IL_0062:  call       string [mscorlib]System.String::Concat(string, string)


  실제 Concat 라는 메서드를 한번 보기로 하자.

public static string Concat(string str0, string str1)

{

    if (string.IsNullOrEmpty(str0))

    {

        if (string.IsNullOrEmpty(str1))

        {

            return string.Empty;

        }

        return str1;

    }

    if (string.IsNullOrEmpty(str1))

    {

        return str0;

    }

    int num1 = str0.Length;

    string text1 = string.FastAllocateString(num1 + str1.Length);

    string.FillStringChecked(text1, 0, str0);

    string.FillStringChecked(text1, num1, str1);

    return text1;

}


  소스에서 눈여겨 봐야 할 부분은 FastAllocateString이라는 메서드 이다. 여기에서 보이는 것과 같이 새로 메모리를 할당하는 것을 볼 수 있다. 실제 Concat 메서드를 통해 볼 수 있듯이 새로운 문자열과 결합해서 문자열을 생성해내야 하는 문자열 연산은, 연산이 이루어질때마다 새로운 string 개체를 생성하고 스택 내 메모리를 재할당하는 과정을 반복하게 된다. 문자열에 대해 증가에 대해서 “A"라는 문자를 증가시킬 때 인덱스가 2씩 증가할 때 마다 메모리가 증가되는 이유는 char 형이 실제 2바이트를 이용하지만 할당되는 메모리 단위기 4바이트씩 증가되기 때문에 실행결과 그림과 같이 보인다.

  string 타입의 제약은 메모리 재할당 문제로 인해 발생한다. 메모리의 안전한 보호와 동적인 수정이 가능한 구조로 가기 위해 실제 string 타입이 선택한 방법은 수정요구시 마다, 새로운 string 타입을 생성하고 적절한 사이즈로 메모리를 할당하고 문자열을 배정하는 것이라고 보면 된다.


  StringBuilder

  처음 나온 예제소스에서 보면 17번 반복해서 문자열을 한자씩 더해가고 있다. 그 이유는 StringBuilder 개체를 생성할 때, 초기에 그 크기를 선언하지 않으면 16으로 크기가 자동으로 결정된는 것을 보여주기 위해서 문자열의 크기를 맞추어 보았다. 출력 결과를 보면 “A"라는 문자가 16개가 된 후 StringBuilder 개체 역시 메모리 재 할당 과정을 수행하게 된다. 따라서 테스트용으로 작성된 소스의 17번째 반복 시점에서 메모리 사용이 증가하는 것을 발견할 수 있게 된다.

  메모리 재할당이 이루어지는 단계에서는 새로운 StringBuilder 개체 생성과 기존 크기의 2배 크기로 메모리 할당, 기존 데이터의 복사가 이루어진다.


  자 이제, StringBuilder 개체 생성시 적절한 사이즈를 설정한 후 작업을 하게 되면 재할당 과정에 드는 리소스의 사용을 최소화 할 수 있다는 것을 알 수 있을 것이다.


  StringBuilder에 대한 좀더 상세한 내용은 다음에 더 다루어 보도록 하겠다.



  결론

  string과 StringBuilder는 비슷한 용도로 활용되면서, 많은 차이점을 가지고 있다. 일반적으로 얼마되지 않는 스트링 연산과 데이터를 이용하는데 있어서 string이 성능상 더 나을 수도 있다. 그럼 StringBuilder을 왜 사용하는 것일까?


  아래의 간단한 성능 테스트 코드를 보자.

class Program

{

    static void Main(string[] args)

    {

        StringBuilder strBuilder = new StringBuilder(100000);

        string str1 = ""

 

        // 시간 측정을 위한 코드

        Stopwatch stopWatch1 = new Stopwatch();

        Stopwatch stopWatch2 = new Stopwatch();

        TimeSpan time1, time2;

 

        // string object

        Console.WriteLine("\nString Object");

 

        stopWatch1.Start();

        for (int i = 0; i < 100000; i++)

        {

            str1 = str1 + "A"

        }

        stopWatch1.Stop();

 

        time1 = stopWatch1.Elapsed;

        Console.WriteLine("string 반복시간 : " +

                        time1.Milliseconds.ToString());

 

        // StringBuilder object

        Console.WriteLine("\nStringBuilder Object");

        stopWatch2.Start();

        for (int i = 0; i < 100000; i++)

        {

            strBuilder.Append("A");

        }

        stopWatch2.Stop();

 

        time2 = stopWatch2.Elapsed;

        Console.WriteLine("StringBuilder 경과 시간 : " +

                         time2.Milliseconds.ToString());

   }

}


  위의 소스는 시간 측정을 하기 위해 System.Diagonstics 의 Stopwatch 클래스를 이용하는 코드와 결과를 명확히 하기 위해 반복 횟수를 100000으로 조절하여 테스트 한 소스이다. 변수 할당같은 경우 소요시간이 엄청 짧기 때문에 반복횟수를 크게 했다. 결과는 아래와 같다.

 

그림. 실행결과


  동일한 100000번의 반복에서 경과시간을 비교해볼 수 있을 것이다. 이렇듯 많은 연산이 요구되는 경우 string은 성능상에 많은 문제를 일으킬 수 있다는 것을 보여주고 싶었다.


  많은 연산이 수행되는 경우 효과적인 성능을 보장할 수 있기에 StringBuilder 클래스를 이용해서 작업을 수행하는 것이다. 적절한 크기를 명시하고, 적절한 장소에서 이용된다면 편리하고 시스템에 덜 부담을 주는 코드를 만들어 낼 수 있게 된다.

  적당한 것 예를 들어, 데이터베이스 컨넥션 스트링이나 기타 문자열 결합 형식 등을 통한 일반적인 몇 자 안되는 문자열 생성 등에는 string 타입 변수 선언을 해서 이용하는 것이 이득이고, 대규모의 문자열에서 잦은 수정과 반복을 통한 추가 등이 이루어져야 한다면 StringBuilder을 쓰는게 좋을 수 있다.





리플들



안녕하세요.
글은 잘 읽었습니다. 그리고 궁금한게 있어서요..
만약 StringBuilder의 크기를 주지않고 한번에 더하는 텍스트의 길이가 16이상인 텍스트를 10번 더하는 경우라면, string과 StringBuilder가 같은 성능을 보이게 되는 것인지요?



아~~ 늦게서야 확인했습니다.
요즘은 데브피아에 잘 못들어와서.ㅡ.ㅜ

StringBuilder의 경우, 선언된 크기보다 큰 문자열이 들어올 경우 재할당을 하게되는데 크기의 증가는 배수로 증가하는 로직을 탑니다.^^;

처음에 16, 다음에 32, 64, 128.... 이런식으로요. 경우에 다라서 다르지만 String보다는 여전히 StringBuilder이 유리하죠.^^;

String의 경우 간단하게 보면 10번 개체가 생성되어야 하지만, StringBuilder의 경우 5번 생성된다고 보시면 됩니다.ㅎㅎㅎ




출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=18&MAEULNO=8&no=1229&page=15
Posted by Sting!
,