포스트

메모리베리어 (하드웨어의 최적화 방지)


이번 포스팅은 메모리베리어에 관하여 다루어보겠습니다. 메모리베리어는 지난번에 확인했었던 릴리즈모드에서의 어셈블리어화 최적화를 할 때 코드의 순서가 뒤바뀌는 것과 같은 상황이 릴리즈모드가 아닌데도 하드웨어적으로 일어날 때 사용할 수 있는 방법입니다. 문제가 일어나는 예시 코드를 살펴보겠습니다.

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
using System;
using System.Data;
using System.Threading;

namespace ServerCore
{

    internal class Program
    {
        static int x = 0;
        static int y = 0;
        static int r1 = 0;
        static int r2 = 0;

        static void Thread_1()
        {
            y = 1; // Store y
            r1 = x; // Load x
        }

        static void Thread_2()
        {
            x = 1; // Store x
            r2 = y; // Load y
        }

        static void Main(string[] args)
        {
            int count = 0;
            while (true)
            {
                count++;
                x = y = r1 = r2 = 0;

                Task t1 = new Task(Thread_1);
                Task t2 = new Task(Thread_2);
                t1.Start();
                t2.Start();

                Task.WaitAll(t1, t2);

                if (r1 == 0 && r2 == 0)
                    break;
            }

            Console.WriteLine($"{count}번만에 빠져나옴!");

        }
    }
}

위 코드는 Thread_1과 Thread_2함수가 각각 실행됩니다. 초기값이 0으로 세팅되어있는 x, y, r1, r2에 각각 x=1, y=1을 대입하고 다음으로 각각 r2=y, r2=x을 대입하는 코드입니다.

기존에 알던 싱글쓰레드처럼 생각해보면 당연히 r1=1, r2=1로 빠져나오겠지요? 그러면 if (r1 == 0 && r2 == 0) break조건에 걸릴 수가 없습니다. 이제 코드를 실행해봅시다.

n번만에 어떻게 빠져나올 수 있었을까요? 정답은 이번에도 기계가 하는 최적화 때문입니다. 지난 번에 릴리즈모드에서 어셈블리언어로 소프트웨어적으로 하는 최적화와는 다르게 이번에는 디버그모드에서 하드웨어가 하는 최적화입니다. 이 하드웨어 최적화는 컴퓨터가 생각하기에 더 빠른 방법으로 실행될 것 같도록 알아서 코드의 실행순서를 바꿔주는 것입니다. 이제껏 우리가 눈치채지 못했던 이유는 항상 싱글쓰레드로 코딩을 해왔기 때문입니다. 하드웨어의 최적화는 다음과 같이 일어났습니다.

1
2
3
4
5
 static void Thread_1()
        {
            y = 1; // Store y
            r1 = x; // Load x
        }

위의 코드를 다음과 같이 뒤바꾼 순서로 실행했던 것입니다. 이유는 더 빠르게 처리할 수 있고 이렇게 바꾸어도 함수 내에서 아무 문제 없을 것 같아서 그렇다고 합니다.

1
2
3
4
5
        static void Thread_1()
        {
            r1 = x; // Load x            
            y = 1; // Store y
        }

정리하자면, 릴리즈를 할 때 하는 최적화도 있지만, 릴리즈하지 않아도 하는 하드웨어적인 최적화도 있다는 것입니다. 이 하드웨어적인 최적화는 싱글쓰레드일때는 문제가 되지 않지만 이처럼 멀티쓰레드에서는 주의깊게 살펴야 합니다. 그래서 이러한 순서뒤바뀜을 막기 위해서 우리는 코드에 '배리어'라는 것을 사용할 수 있습니다.

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
using System;
using System.Data;
using System.Threading;

namespace ServerCore
{
    // 메모리 배리어
    // A) 코드 재배치 억제
    // B) 가시성

    // 1) Full Memory Barrier (ASM MFENCE, C# Thread.MemoryBarrier) : Store/Load 둘 다 막는다.
    // 2) Store Memory Barrier (ASM SFENCE) : Store만 막는다.
    // 3) Load Memory Barrier (ASM LFENCE) : Load만 막는다.

    internal class Program
    {
        static int x = 0;
        static int y = 0;
        static int r1 = 0;
        static int r2 = 0;

        static void Thread_1()
        {
            y = 1; // Store y

            // ----------------------------------경계선
            Thread.MemoryBarrier();

            r1 = x; // Load x
        }

        static void Thread_2()
        {
            x = 1; // Store x

            // ----------------------------------경계선
            Thread.MemoryBarrier();

            r2 = y; // Load y
        }

        static void Main(string[] args)
        {
            int count = 0;
            while (true)
            {
                count++;
                x = y = r1 = r2 = 0;

                Task t1 = new Task(Thread_1);
                Task t2 = new Task(Thread_2);
                t1.Start();
                t2.Start();

                Task.WaitAll(t1, t2);

                if (r1 == 0 && r2 == 0)
                    break;
            }

            Console.WriteLine($"{count}번만에 빠져나옴!");

        }
    }
}

C#에서 지원하는 Thread()메소드에서 .MemoryBarrier()함수를 사용하게 되면 순서 상으로 중요한 두 코드 뭉치의 순서를 가시화해서 컴퓨터에게 알려줄 수 있습니다. 이렇게 컴퓨터에게 알려줌으로써 임의적인 코드재배치를 억제할 수 있는 것이죠. 위 코드를 실행해 보는 것으로 마무리하겠습니다.

의도했던 대로 break조건이 발동되지 않아 while문에 갇혀있군요. 코드의 문제를 해결했습니다.


이 글은 인프런의 '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 강의를 참고하여 정리 목적으로 작성하였습니다. 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.