Posts C# String, StringBuilder, ZString
Post
Cancel

C# String, StringBuilder, ZString

String 포맷팅의 문제점


1
$"IntValue : {123}, BoolValue : {true}";

또는

1
string.Format("IntValue {0}, BoolValue : {1}", 123, true);

이런 방식의 스트링 포맷팅을 쓰는 경우가 정말 많다.


정말 편리하긴 하지만,

StringBuilder와 비교하면 성능도 좋지 않고

심지어 가비지도 더 많이 발생시킨다.

그렇다고 StringBuilder를 쓰려니 가독성이 좋지 않고 불편하다는 단점이 있다.


ZString



1
ZString.Format("IntValue {0}, BoolValue : {1}", 123, true);

string.Format()과 같은 형식으로 사용할 수 있는

ZString.Format() 메소드를 제공한다.

그래도 여전히 StringBuilder보다는 느리고 가비지도 많이 생성한다.

그렇지만 string.Format()보다는 더 낫다.


StringBuilder Wrapper


Source Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class STR
{
    private static readonly STR singleton = new STR();
    private readonly StringBuilder sb = new StringBuilder(100);

    private STR() { }

    public static STR Begin()
    {
        singleton.sb.Clear();
        return singleton;
    }

    public STR _(string value) { sb.Append(value); return this; }
    public STR _(bool value)   { sb.Append(value); return this; }
    public STR _(byte value)   { sb.Append(value); return this; }
    public STR _(short value)  { sb.Append(value); return this; }
    public STR _(ushort value) { sb.Append(value); return this; }
    public STR _(int value)    { sb.Append(value); return this; }
    public STR _(uint value)   { sb.Append(value); return this; }
    public STR _(float value)  { sb.Append(value); return this; }
    public STR _(double value) { sb.Append(value); return this; }

    public string End()
    {
        return sb.ToString();
    }
}


StringBuilder를 아주 조금이라도 편하게 사용하기 위해 시험삼아 작성해본 클래스.

매번 번거롭게 StringBuilder 객체를 만들어 쓰는 대신

싱글톤 객체를 만든 다음 정적 호출에 숨겨버리고,

1
2
3
4
sb.Append("Int : ")
  .Append(a)
  .Append(", Bool : ")
  .Append(b);

이런식으로 작성할 코드를 좀더 타이트하게 줄여서

1
STR.Begin()._("Int : ")._(a)._(", Bool : ")._(b).End();

이렇게 그나마 한 줄로 나열될 수 있게 했다는 의의가 있지만

가독성은 여전히 썩 좋지 않다.

이럴 때는 C/C++의 전처리 매크로를 C#에서도 쓰고 싶다는 생각이 강하게 든다.


Benchmark


벤치마크 소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private StringBuilder sb = new StringBuilder(200);
private int intValue = 123;
private bool boolValue = true;
private float floatValue = 1234.567f;

[Benchmark(Baseline = true)]
public string StringFormat_1()
{
    return $"IntValue : {intValue}, BoolValue : {boolValue}, FloatValue : {floatValue}";
}

[Benchmark]
public string StringFormat_2()
{
    return string.Format("IntValue : {0}, BoolValue : {1}, FloatValue : {2}", intValue, boolValue, floatValue);
}

[Benchmark]
public string StringBuilder_()
{
    sb.Clear();
    return sb
        .Append("IntValue : ")
        .Append(intValue)
        .Append(", BoolValue : ")
        .Append(boolValue)
        .Append(", FloatValue : ")
        .Append(floatValue)
        .ToString();
}

[Benchmark]
public string ZString_()
{
    return ZString.Format("IntValue : {0}, BoolValue : {1}, FloatValue : {2}", intValue, boolValue, floatValue);
}

[Benchmark]
public string StringBuilderWrapper()
{
    return STR.Begin()
        ._("IntValue : ")._(intValue)
        ._(", BoolValue : ")._(boolValue)
        ._(", FloatValue : ")._(floatValue)
        .End();
}


결과

image


추가 : 가비지 생성량(byte)

  • string.Format() : 208
  • ZString.Format() : 160
  • StringBuilder : 136


결론

  • 성능, 가비지 면에서 언제나 StringBuilder가 가장 좋다.

  • ZString.Format()string.Format()보다 더 좋다.

  • 가독성을 포기하고 성능을 선택하는 경우, StringBuilder를 쓰면 된다.

  • 반드시 스트링 포맷팅이 필요한 경우, ZString.Format()를 쓰면 된다.

  • string.Format()은 안쓰면 된다.


추가 벤치마크 : StringBuilder.AppendFormat()

..

StringBuilder 클래스에는 string.Format()처럼 스트링을 포맷팅하여 추가하는 .AppendFormat() 메소드가 있다.

사용법은 string.Format()과 동일하며,

가비지도 string.Format()과 동일하게 생성한다.

위와 동일한 조건으로 벤치마크를 진행해보았다.

image

image

벤치마크 루프 횟수를 다르게 지정하여 각각 수행했지만,

string.Format()과 거의 비슷한 성능이 나오는 것을 확인할 수 있었다.


너무나 동일하기에 StringBuilder.AppendFormat()string.Format()의 내부 구현을 확인해보니,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* String.Format() */

// System.String
/// <summary>문자열에 있는 서식 지정 항목을 지정된 세 개체의 문자열 표현으로 바꿉니다.</summary>
[__DynamicallyInvokable]
public static string Format(string format, object arg0, object arg1, object arg2)
{
    return FormatHelper(null, format, new ParamsArray(arg0, arg1, arg2));
}

// System.String
using System.Text;

private static string FormatHelper(IFormatProvider provider, string format, ParamsArray args)
{
    if (format == null)
    {
        throw new ArgumentNullException("format");
    }
    return StringBuilderCache.GetStringAndRelease(StringBuilderCache.Acquire(format.Length + args.Length * 8).AppendFormatHelper(provider, format, args));
}

// System.Text.StringBuilder
internal StringBuilder AppendFormatHelper(IFormatProvider provider, string format, ParamsArray args)
{
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* StringBuilder.AppendFormat() */

// System.Text.StringBuilder
/// <summary>서식 항목이 0개 이상 포함된 복합 서식 문자열을 처리하여 반환된 문자열을 이 인스턴스에 추가합니다. 각 서식 항목이 세 인수 중 하나의 문자열 표현으로 바뀝니다.</summary>
[__DynamicallyInvokable]
public StringBuilder AppendFormat(string format, object arg0, object arg1, object arg2)
{
    return AppendFormatHelper(null, format, new ParamsArray(arg0, arg1, arg2));
}

// System.Text.StringBuilder
internal StringBuilder AppendFormatHelper(IFormatProvider provider, string format, ParamsArray args)
{
    // ...
}


애초에 내부적으로 StringBuilder.AppendFormatHelper() 메소드를 동일하게 호출하고 있음을 알 수 있었다.


결론

  • string.Format(), StringBuilder.AppendFormat() 메소드의 내부 구현은 같다.


Benchmark 2


  • ZString에도 스트링 빌더가 존재하는데, 깜빡했다.
  • 따라서 이번에는 StringBuilder, ZString의 Utf16ValueStringBuilder를 이용해 벤치마크를 수행한다.


[1] 벤치마크 소스코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private StringBuilder sb;
private Utf16ValueStringBuilder zb;
private int intValue = 123;
private bool boolValue = true;
private float floatValue = 1234.567f;

[GlobalSetup]
public void Init()
{
    sb = new StringBuilder(500);
    zb = ZString.CreateStringBuilder();
}

[Benchmark(Baseline = true)]
public string StringBuilder_Append()
{
    sb.Clear();
    return sb
        .Append("IntValue : ")
        .Append(intValue)
        .Append(", BoolValue : ")
        .Append(boolValue)
        .Append(", FloatValue : ")
        .Append(floatValue)
        .ToString();
}

[Benchmark]
public string ZStringBuilder_Append()
{
    zb.Clear();
    zb.Append("IntValue : ");
    zb.Append(intValue);
    zb.Append(", BoolValue : ");
    zb.Append(boolValue);
    zb.Append(", FloatValue : ");
    zb.Append(floatValue);

    return zb.ToString();
}

[Benchmark]
public string StringBuilder_AppendFormat()
{
    sb.Clear();
    return sb.AppendFormat("IntValue : {0}, BoolValue : {1}, FloatValue : {2}",
        intValue, boolValue, floatValue).ToString();
}

[Benchmark]
public string ZStringFormat()
{
    return ZString.Format("IntValue : {0}, BoolValue : {1}, FloatValue : {2}",
        intValue, boolValue, floatValue);
}


[2] 결과

image

  • .Append()StringBuilder, Utf16ValueStringBuilder 모두 힙 할당이 없음을 알 수 있다.
    (136 byte의 힙 할당은 .ToString()에 의해 발생한다.)

  • 성능은 비슷하다.


Note


환경마다 StringBuilder의 동작이 조금 다른 듯하다.

예를 들어 다음 코드를 실행했을 때,

1
2
3
4
5
6
StringBuilder sb = new StringBuilder(1000);

for (int i = 0; i < 100; i++)
{
    sb.Append(i);
}

콘솔 앱에서는 위 반복문의 StringBuilder.Append(int)에 의한 힙 할당이 없다.

.NET Framework 2.0, 4.0, 4.7.2, .NET Core 3.1 버전에서 테스트 해보았지만 모두 동일했다.

그런데 유니티 엔진에서는 100번의 힙 할당이 발생하며 그 크기는 대략 3.3kB 정도다.


만약 유니티 엔진을 사용한다면 ZString을 꼭 사용하는 것이 좋을 것 같다.

This post is licensed under CC BY 4.0 by the author.