Posts C# - if-else if vs switch-case vs Dictionary
Post
Cancel

C# - if-else if vs switch-case vs Dictionary

분기 처리


특정 변수의 값에 따라 분기를 나누어 처리해야 하는 경우,

일반적으로 if-else if 또는 switch-case문을 사용할 수 있다.

1
2
3
4
5
int value = someValue;

if(value == 0) DoSomething0();
else if(value == 1) DoSomething1();
else if(value == 2) DoSomething2();
1
2
3
4
5
6
7
8
int value = someValue;

switch(value)
{
    case 0 : DoSomething0() break;
    case 1 : DoSomething1() break;
    case 2 : DoSomething2() break;
}

그리고 이를 Dictionary를 통해 처리할 수도 있다.

1
2
3
4
5
6
7
// 딕셔너리 생성
Dictionary<int, Action> dict = new();

// 분기에 따른 처리 추가
dict.Add(0, DoSomething0);
dict.Add(1, DoSomething1);
dict.Add(2, DoSomething2);
1
2
3
4
int value = someValue;

// 처리 수행
dict[value].Invoke();


특징


[1] if-else if

정해진 상수들에 대한 분기 처리는 컴파일 타임에 확정될 수밖에 없으며,

조건 값들을 저장하는 컬렉션과 for 또는 foreach를 활용하면

런타임에도 조건을 변경하여 사용할 수 있긴 하다.

하지만 어쨌든, 어느 경우든 간에 if-else if의 나열을 이용한 동일 값 비교는

대안이 없는 경우가 아니라면, 가독성, 성능, 유지보수 모든 면에서 좋지 않은 방법이다.


[2] switch-case

switch-case를 통한 분기 처리는 컴파일 타임에 확정할 수밖에 없다.

만약, 런타임에 분기에 따른 처리를 추가/제거하려면 Dictionary를 써야만 한다.

switch-case는 실제로 if-else if로 해석되어 처리된다고 한다.

그리고 정수 또는 enum 타입의 경우, 각 case의 숫자가 일정 범위 내에서 연속되면

점프 테이블을 형성하여 if-else if보다 더 빠르게 처리된다.


[3] Dictionary

컴파일 타임, 런타임에 분기를 추가하거나 제거할 수 있다.

대신, Key로부터 해시 계산을 하여 Value를 찾아오기 때문에

만약 해시 계산이 비싼 string의 경우 성능 차이가 크게 날 수 있다.

해시 계산이 저렴한 정수나 enum이라고 해도,

어쨌든 Dictionary 내부의 엔트리를 검색하는 과정을 거치기 때문에

점프 테이블을 생성하는 switch-case보다는 느릴 것으로 예상된다.


성능 테스트


  • Benchmark Dotnet을 통해 테스트한다.

  • 점프 테이블을 생성하는 enum, 생성하지 않는 string 타입에 대해 테스트한다.

  • 단순히 참조 오버헤드만 비교하기 위해, 분기마다 실행되는 메소드는 최대한 간단히 작성한다.


테스트 1 : enum


테스트 코드

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public enum MyEnum
{
    A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11,
}

[ParamsAllValues]// 모든 enum 순회
public MyEnum currentKey;

public Dictionary<MyEnum, Action> dict;
public int dest;

[GlobalSetup]
public void GlobalSetup()
{
    dict = new Dictionary<MyEnum, Action>();

    MyEnum[] allEnumValues =
        Enum.GetValues(typeof(MyEnum))
        .Cast<MyEnum>()
        .ToArray();

    foreach (var e in allEnumValues)
    {
        dict.Add(e, Job);
    }
}

[Benchmark(Baseline = true)]
public void Switch_Case()
{
    switch (currentKey)
    {
        case MyEnum.A0: Job(); break;
        case MyEnum.A1: Job(); break;
        case MyEnum.A2: Job(); break;
        case MyEnum.A3: Job(); break;
        case MyEnum.A4: Job(); break;
        case MyEnum.A5: Job(); break;
        case MyEnum.A6: Job(); break;
        case MyEnum.A7: Job(); break;
        case MyEnum.A8: Job(); break;
        case MyEnum.A9: Job(); break;
        case MyEnum.A10: Job(); break;
        case MyEnum.A11: Job(); break;
    }
}

[Benchmark]
public void If_Else()
{
    if (currentKey == MyEnum.A0) Job();
    else if (currentKey == MyEnum.A0) Job();
    else if (currentKey == MyEnum.A1) Job();
    else if (currentKey == MyEnum.A2) Job();
    else if (currentKey == MyEnum.A3) Job();
    else if (currentKey == MyEnum.A4) Job();
    else if (currentKey == MyEnum.A5) Job();
    else if (currentKey == MyEnum.A6) Job();
    else if (currentKey == MyEnum.A7) Job();
    else if (currentKey == MyEnum.A8) Job();
    else if (currentKey == MyEnum.A9) Job();
    else if (currentKey == MyEnum.A10) Job();
    else if (currentKey == MyEnum.A11) Job();
}

[Benchmark]
public void Dictionary()
{
    dict[currentKey].Invoke();
}

private void Job() => dest = (int)currentKey;


결과

image

switch-case는 값이 달라져도 언제나 일정한 시간을 소요하고,

if-else는 조건식을 많이 거칠수록 더 많은 시간을 소요했다.

그리고 Dictionaryswitch-case처럼 일정했지만

수행 시간이 4배 이상으로 더 많이 소요되는 것을 확인할 수 있었다.


테스트 2 : string


테스트 코드

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public IEnumerable<object> stringKeys()
{
    yield return "0000000000";
    yield return "0000000001";
    yield return "0000000002";
    yield return "0000000003";
    yield return "0000000004";
    yield return "0000000005";
    yield return "0000000006";
    yield return "0000000007";
    yield return "0000000008";
    yield return "0000000009";
}

[ParamsSource(nameof(stringKeys))]
public string currentKey;

public Dictionary<string, Action> dict;
public volatile int dest;

[GlobalSetup]
public void GlobalSetup()
{
    dict = new Dictionary<string, Action>();

    foreach (string item in stringKeys())
    {
        dict.Add(item, Job);
    }
}

[Benchmark(Baseline = true)]
public void Switch_Case()
{
    switch (currentKey)
    {
        case "0000000000": Job(); break;
        case "0000000001": Job(); break;
        case "0000000002": Job(); break;
        case "0000000003": Job(); break;
        case "0000000004": Job(); break;
        case "0000000005": Job(); break;
        case "0000000006": Job(); break;
        case "0000000007": Job(); break;
        case "0000000008": Job(); break;
        case "0000000009": Job(); break;
    }
}

[Benchmark]
public void If_Else()
{
    if (currentKey == "0000000000") Job();
    else if (currentKey == "0000000001") Job();
    else if (currentKey == "0000000002") Job();
    else if (currentKey == "0000000003") Job();
    else if (currentKey == "0000000004") Job();
    else if (currentKey == "0000000005") Job();
    else if (currentKey == "0000000006") Job();
    else if (currentKey == "0000000007") Job();
    else if (currentKey == "0000000008") Job();
    else if (currentKey == "0000000009") Job();
}

[Benchmark]
public void Dictionary()
{
    dict[currentKey].Invoke();
}

private void Job() => dest = 123123;


결과

image

switch-case보다 Dictionary가 더 오랜 시간이 소요되는 것은 여전했다.

그런데 switch-caseif-else if로 해석될 것이라고 생각했지만,

단순히 if-else if를 통해 문자열들을 직접 비교하는 것이 아니라

어떤 최적화를 통해 더 빠르게 비교를 수행하고 분기를 처리하는 것으로 보인다.

그리고 if-else if는 역시 모든 조건식을 순차적으로 탐색하므로

검사하는 조건식이 많을수록 더 많은 시간을 소요한다.


추가 : 문자열에 대한 switch-case 문 최적화

어셈블리를 디컴파일하여 CIL을 열어보면 switch-case는 시작 부분에

1
call       uint32 '<PrivateImplementationDetails>'::ComputeStringHash(string)

이런 부분이 있다.

string을 매개변수로 받아 uint 타입의 해시 결과를 리턴하는 메소드로 보인다.

이를 자세히 확인하기 위해 DotPeek을 이용해 컴파일러가 생성하는 코드를 살펴보았다.

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
56
57
public void Switch_Case()
{
  string currentKey = this.currentKey;
  switch (\u003CPrivateImplementationDetails\u003E.ComputeStringHash(currentKey))
  {
    case 1434475458:
      if (!(currentKey == "0000000009"))
        break;
      this.Job();
      break;
    case 1451253077:
      if (!(currentKey == "0000000008"))
        break;
      this.Job();
      break;
    case 1468030696:
      if (!(currentKey == "0000000007"))
        break;
      this.Job();
      break;
    case 1484808315:
      if (!(currentKey == "0000000006"))
        break;
      this.Job();
      break;
    case 1501585934:
      if (!(currentKey == "0000000005"))
        break;
      this.Job();
      break;
    case 1518363553:
      if (!(currentKey == "0000000004"))
        break;
      this.Job();
      break;
    case 1535141172:
      if (!(currentKey == "0000000003"))
        break;
      this.Job();
      break;
    case 1551918791:
      if (!(currentKey == "0000000002"))
        break;
      this.Job();
      break;
    case 1568696410:
      if (!(currentKey == "0000000001"))
        break;
      this.Job();
      break;
    case 1585474029:
      if (!(currentKey == "0000000000"))
        break;
      this.Job();
      break;
  }
}

case 값들이 문자열로 되어 있지 않고, 정수 값으로 되어 있다.

일단 컴파일 전 소스 코드의 case 문자열들을 해시 계산을 통해 미리 정수로 바꿔놓고,

런타임에 switch-case문이 실행될 때마다 입력되는 문자열을

ComputeStringHash(string) 메소드에 넣고 uint 정수를 반환받아

그 값을 통해 해당하는 case로 점프하여 실행하는 것으로 보인다.

그리고 각 case에 도달했더라도, 문자열이 정확히 일치하는지 재확인하는 코드도 확인할 수 있다.


최종 결론


  • 컴파일 타임에 상수로 분기를 나누어 처리한다면 switch-case의 성능이 가장 좋다.

  • 런타임에 분기를 나누어 처리한다면 Dictionary를 사용하는 것이 좋다.

  • if-else if는 이런 방식으로는 쓰지 않는 것이 좋다.


References


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