공부/인프런 - Rookiss

Part 4-2-4. 멀티쓰레드 프로그래밍 : 캐시 이론

셩잇님 2023. 9. 6. 02:00
반응형

 

 

멀티 쓰레드

 

캐시

지난번과 동일하게 패밀리 레스토랑을 예로 들어 이해해보자 👍

 

 지난시간 우리는 패밀리 레스토랑인 아웃백을 개업하고 직원(로봇)을 채용해 영혼을 통해 로봇을 움직였다. 그러나 레스토랑이 궤도에 올라서 손님도 많아지고 돈도 많아졌는데 손님이 몰리다보니 일처리를 어떻게 해야할지 고민이 생긴것이다.

 

 

 오른쪽을 보면 주문 현황을 볼 수 있다. 테이블을 돌아다니며 주문을 받는 직원이 있는데 주문을 받으면 주방으로 전달을 해야 주문이 들어간다. 이 식당에는 주문 현황 기계가 있는데 이 기계에 주문을 기입하면 주방에서 주문서가 나와 음식 조리가 시작된다. 

 

 그런데 하나의 문제가 있다. 주문 현황판이 멀리 있다는 것이다. 그렇다면 직원은 이를 어떻게 해결하는 것이 가장 좋을까? 다양한 해결 방법이 있다.

 

1. 단기 기억, 2. 미니 수첩, 3. 대형 수첩

 주문을 받자마자 주문 현황판에 작성하는 것이 아닌, 주문을 모아서 작성하고 현황판에 작성하는 것이다. 주문이 많아지면 수첩에 적고, 이보다 더 많아지면 대형 수첩에 적는 것이다. 실제로 우리가 식당에 방문할 때에도 위와 같은 방법으로 이루어지는 것을 볼 수 있다.

 

 예를 들어 우리 식당인 아웃백에서 직원 A가 2번 테이블에서 콜라를 시키면 주문을 모았다가 주문 현황판에 추가하면 된다. 그러나 여기서 문제가 생긴다. 2번 테이블에서 콜라가 아닌 사이다로 변경하겠다고 직원 B를 불러서 요청을 하는 것이다. 그러면 직원 B는 직원 A가 수첩에 적은 콜라 주문을 받고 머리속이 '???'가 되는 것이다. 자기는 콜라 주문을 받은 적도 없고, 가게 현황판에는 콜라를 주문한 흔적이 없기 때문이다. 즉 2번 테이블에서 주문한 음식이 최종적으로 주문이 갱신되는 것이 아니므로 이렇게 알바마다 혼선이 올 수 있는 것이다.

 

 

 그러나 이 모든 행동도 컴퓨터에서 똑같이 일어난다. 코어는 연산을 위한 ALU와 기억을 하는 용도로 쓰이는 캐시가 존재하는데 위 두 개의 항목이 세트로 이루어져있다. 가게 현황판이 멀리 있는 것과 같이 우리의 RAM은 컴퓨터 내부에서 물리적으로 멀리 떨어져있기 때문에 매번 메모리를 갱신하는 것은 너무나 무겁고 힘든일이다.

 

 따라서 기억과 수첩의 대응하는 개념으로 레지스터와 L1/L2 캐시 기억장치가 있다. 어떤 변수나 메모리의 값을 조작한다고 할 경우 실제 메모리에 바로 변경되는 것이 아닌 나중에 시간이 지나면 메모리에 적재된다.

 


 

캐시의 철학

 캐시는 사실 말하지 않아도 내부적으로 이미 알아서 잘 작동하고 있다. 그렇다면 캐시는 무엇을 캐싱할까?

 

1. TEMPORAL LOCALITY

👉 시간적으로 보면 방금 주문한 테이블에서 추가 주문이 나올 확률이 높다. 방금 주문한 것을 메모해 놓으면 편하지 않을까? 예) 주문 다하고 다먹고 떠드는 테이블보다 가게에 들어와 주문을 시작한 테이블이 추가적으로 주문할 확률이 높다

 

2. SPACIAL LOCALITY

👉 공간적으로 보면, 방금 주문한 사람 근처에 있는 사람이 추가 주문할 확률이 높다. 방금 주문한 사람과 합석하고 있는 사람들의 주문 목록도 메모해 놓으면 편하지 않을까? 예) 회식할 때 단체로 앉으면 옆자리나 주변자리도 주문한다.

 

이는 정보처리기사 지역성과 유사한 개념으로 보인다. 🤔 그런데 싱글 스레드는 알아서 처리하니 우리는 편하게 돈을 벌 수 있는데 멀티 스레드는 다르다.. 😭 위와 설명한 것과 같이 서로의 주문 현황이 다르므로 문제가 생기는데 이는 컴퓨터에서도 똑같이 동작한다.. 

 


 

실습

캐시는 정말 잘 작동하는가?를 확인하기 위한 테스트이다.

 

using System;
using System.Threading;

namespace ServerCore
{
    class Program
    {
        static void Main(string[] args)
        {
            // 캐시가 정말 잘 작동하는가? 확인을 위한 테스트
            int[,] arr = new int[10000, 10000];
            
            // 순회방향 : 👉
            {
                long now = DateTime.Now.Ticks;
                for (int i = 0; i < 10000; i++)
                    for (int j = 0; j < 10000; j++)
                        arr[i, j] = 1;
                long end = DateTime.Now.Ticks;
                Console.WriteLine($"(i, j 순서 걸린 시간 {end - now}");
            }

            // [] [] [] [] []
            // [] [] [] [] []
            // [] [] [] [] [] 
            // 0, 0를 기점으로 1,0 > 2,0 > 3,0으로 접근한다.
            // 순회 방향 : ↑
            {
                long now = DateTime.Now.Ticks;
                for (int i = 0; i < 10000; i++)
                    for (int j = 0; j < 10000; j++)
                        arr[j, i] = 1;
                long end = DateTime.Now.Ticks;
                Console.WriteLine($"(j, i 순서 걸린 시간 {end - now}");
            }
        }
    }
}

 

일반적으로 수학적으로 생각해본다면 우리는 두개의 for문이 동일한 시간이 소요될 것이라고 생각한다. 디버그 모드를 통해 확인해보자. 과연 그럴까?

 

대략 2배의 시간이 차이가난다. 왜 그럴까?

 

 위에서 학습한 캐시 공간 지역성을 살펴보면 어떤 변수에 접근하면 인접한 주소는 접근 확률이 높으므로 캐시가 캐시를 진행한다. 따라서 위 배열을 주석과 같이 그림으로 살펴보자. 

 

 10000번의 순회를 이해하기 쉽게 간략히 5*5라고 가정해보자. 첫번째 포문은 순회방향이 👉로 설정되어 순차적으로 접근이 가능하다. 이는 캐시에서는 지역성 법칙에 의해 아! 곧 인접 주소를 접근하겠구나~ 싶어서 캐시로 들고있다. 따라서 이미 다음 인덱스로 접근할 때에는 이미 캐시에 있으므로 캐시 히트 상황이라 빠르게 접근할 수 있다.

 

 그렇지만 두 번째 포문은 순회 방향이 위로 되어있어 상대적으로 느리다. 따라서 공간 지역성의 법칙을 살릴 수 없다. 즉 캐시를 활용할 수 없는 상황이다. 따라서 밀티 쓰레드가 아닌 환경에서도 캐시는 정상적으로 잘 작동하며, 멀티 쓰레드로 넘어갈수록 이 상황은 더욱 끔찍해진단다.. 😲..

 

 

 

반응형