반응형

이 질문에 대해 생각해보자.


* 멀티 프로세서 환경에서는 각 프로세서가 프로세스를 작동시킬 때 

독립된 Cache 공간을 가기 때문에 Cache Miss를 단축시킬 수 있다? * 


기존 프로그램을 병렬 하드웨어에서 잘 수행되게 고치는 것이 어렵기 때문에, 모든 프로세서들이 공유하는 단일 실제 주소 공간을 제공하는 식으로 해결하려고 했다. 이렇게 되면 프로그램들은 어디에서 수행되는지 신경 쓸 필요없이 병렬적으로 수행될 수 있다. 즉, 프로그램의 모든 변수들은 언제, 어느 프로세서에서든 접근이 가능하게 된다. 


다른 방식은 프로세서마다 독립적인 주소 공간을 갖도록 하고 공유할 것이 있다면 무엇인지를 명시적으로 지정하는 방식도 있다. 


실제 주소 공간이 공유될 때는 멀티코어에서는 일반적인 방식인데, 모든 프로세서가 공유 메모리를 똑같이 보도록 하드웨어적인 법으로 캐쉬 일관성을 보장하는 것이 일반적이다. 


이때 공유된 데이터는 캐쉬를 사용할 때 새로운 문제를 야기한다. 

이는 두개의 서로 다른 프로세서는 각자 자신의 캐쉬를 통하여 메모리를 접근하게 되고, 주의하지 않으면 두 개의 다른 값을 갖게 된다이러한 문제점을 일반적으로 캐쉬 일관성(Cache Coherence)문제라고 한다. 


메모리에서의 작업은 읽기보다 쓰기가 매우 중요하다. 왜냐하면 읽기는 메모리의 값을 바꾸기 않지만, 쓰기는 값을 바꾸기 때문이다. 따라서 멀티 프로세서에서 공유 메모리를 다룰때에는 이러한 쓰기 작업을 순서화하는 작업이 매우 중요하다. 


왜냐하면, 쓰기 순서화를 통해서 프로세서들은 각자의 명령을 처리하는 시간에 맞게 시간 순서에 맞는 올바른 메모리 값을 가져올 수 있기 때문이다. 즉, 읽기의 순서는 꼬여도 어짜피 메모리 값을 변하게 하지 않기 때문에 스케쥴링에 의해 순서가 어긋나도 상관이 없지만, 쓰기만은 절대적으로 시간에 맞게 딱딱 스케쥴링이 순서적으로 이루어져야 되는 것이다. 이를 쓰기 순서화(Write Serialization)이라고 한다. 


캐쉬 일관성을 유지하기 위해 멀티 프로세서에서는 캐쉬에게 공유된 데이터의 이동(Migration)과 복사(Replication) 기능을 제공해 준다. 

이동(Migration)이란, 데이터를 지역 캐쉬로 이동하는 것을 의미한다. 이동은 먼 곳에 위치한 공유 데이터를 접근할 때 발생하는 지연과 공유 메모리 요청에 따른 대역폭을 줄여준다. 

복사(Replication)은 공유 데이터가 동시에 읽혀질 때, 캐쉬는 지역 캐쉬 안에 복사본을 만든다. 복사는 공유 데이터 접근 시 대기시간과 읽기 경쟁을 감소시킨다. 


이같은 이동과 복사를 지원하는 것은 공유 데이터를 접근하는 성능에 있어서 매우 중요하기 때문에 많은 멀티 로세서들이 캐쉬 일관성을 유지하기 위해 하드웨어 프로토콜을 도입하고 있다. 멀티 프로세서에서 일관성을 유지하기 위한 프로토콜은 캐쉬 일관성 유지 프로토콜(Cache Coherence Protocol)이라고 부른다. 핵심은 데이터 블록의 모든 공유 상태를 추적하는 것이다. 


가장 널리 사용되는 기법은 스누핑(Snooping)이다. 모든 캐쉬는 실제 메모리 블록으로부터 데이터의 복사본 갖고 있고, 블록의 공유상태에 대한 복사본도 갖고 있다. 그러나 중앙에서 관리되는 상태는 유지하지 않는다. 캐쉬들은 전송 매체(버스 또는 네트워크)를 통하여 모두 접근 가능하다. 그리고 모든 캐쉬 제어기들은 버스에 요청된 블록의 복사본을 갖고 있는지 없는지를 확인하기 위해 매체를 감시(Snoop)한다.


스누핑 프로토콜

스누핑 프로토콜에 대해서 자세히 알아보자. 일관성을 유지하는 한 가지 방법은 프로세서가 데이터를 쓰기 전에 그 데이터에 배타적인 접근을 하는 것이다. 이러한 형태의 프로토콜은 쓰기 작업시 다른 캐쉬에 있는 복사본들을 무효화시키기 때문에 쓰기 무효화 프로토콜(Write Invalidate Protocol)이라고 부른다. 배타적 접근은 쓰기 작업이 일어날 때 읽기 또 쓰기가 가능한 데이터의 복사본들이 없다는 것을 보장해준다. 다른 캐쉬의 모든 복사본들은 무효화된다. 이 말을 쉽게 풀어보면, CPU A가 메모리 상의 X의 데이터를 쓰기할 경우, 쓰기 전에 CPU B가 캐쉬에 가지고 있던 X의 값을 없는 값으로 해버리는(Invalidate) 것이다. 즉, 쓰기 이후에 CPU B가 다시 X에 읽기를 요청하면, 다시 캐쉬 실패(Cache Miss)가 일어나고, 메모리값에서 정확한 값을 읽어오게 되어 일관성을 유지하는 것이다. 이것이 스누핑 버스(Snooping Bus)에서 동작하기 때문에 스누핑 프로토콜인 것이다. 스누핑 버스에서는 데이터의 소유권을 추적할 수 있으며, 블록이 교체될 때 나중 쓰기(Write Back)를 수행하는 경우 블록이 공유되었음을 나타내는 '소유(Owner)'라 불리는 추가적인 상태(State)의 도입을 필요로 한다. 소유 프로세서(Owning Processor)는 블록을 바꾸거나 교체할 때 모든 다른 프로세서와 메모리를 갱신할 책임을 지니고 있다. 스누핑 프로토콜 역시 쓰기 작업에 대해서는 제일 마지막에 수행된 쓰기 작업에 대한 결과값으로 갱신할 필요가 있으며, 쓰기 순서화를 따른다. 따라서 마지막 쓰기가 된 값을 위해서 다른 프로세서의 캐쉬의 해당 메모리 값은 모두 무효화시키는 것이다. 블록 크기는 캐쉬 일관성 유지에 큰 역할을 미친다. 예를 들어 8개 워드가 하나의 블록으로 사용되는 캐쉬에 대한 스누핑에서 두 개의 프로세서가 한 개의 워드를 교대로 읽고 쓴다고 가정하자. 대부분의 프로토콜은 프로세서 사이에 전체 블록을 교환하게 되고, 일관성 유지에 필요한 대역폭이 증가하게 된다. 큰 블록은 또한 거짓 공유(False Sharing), 즉 관련 없는 두 공유 변수가 같은 캐쉬 블록에 있을 때, 각 프로세서가 서로 다른 변수를 접근하더라도 블록 전체가 프로세서 사이에서 교환되어야 하는 문제를 야기할 수 있다. 프로그래머와 컴파일러는 거짓 공유를 피하기 위해 데이터를 신중하게 배치하여야 한다. 쉽게 설명하면, 메모리에서 X라는 위치부터 8개의 워드가 하나의 블록일 때 CPU A에서 X의 위치의 데이터를 메모리에서 캐쉬 미스를 통해 가져오고, 쓰게 된다. 캐쉬 무효화에 의해 CPU B에서 혹시 X에 해당하는 블록이 캐쉬에 있다면, 무효 처리된다. 이번엔 CPU B에서 X+4(word size)위치에 있는 공유 변수를 읽고 쓸때 캐쉬 무효화에 의해서 CPU A에서의 X 블록은 무효화가 된다. 서로 상관이 없는 데이터를 건드렸음에도 불구하고, 다음에 CPU A는 X+8위치의 공유변수를 가져오기 위해 다시 캐쉬 미스로 인해서 메모리로부터 가져와야 되는 비효율을 갖게 된다. 그 다음도 마찬가지 이다.


다시 원점으로 돌아와서, 공유 메모리 멀티 프로세서에서는 모든 프로세서가 단일 실제 주소 공간을 갖는다. 따라서 공유 주소 멀티 프로세서라고 부르는 것이 더 정확할 지도 모른다. 프로세서들이 실제 주소 공간을 공유하더라도 작업들은 각각 별개의 가상 주소 공간에서 수행될 수 있음에 유의해야 한다. 프로세서들은 어느 메모리 주소이든지 Load 와 Store 명령어를 사용하여 접근할 수 있으며, 프로세서간의 통신은 메모리에 있는 공유 변수를 통해 이루어진다. 단일 주소 공간 멀티 프로세서에서는 어느 프로세서가 어느 워드를 접근하든지 동일한 시간을 걸리는 스타일을 균일 메모리 접근(Uniform Memory Access, UMA) 멀티프로세서라 하고, 어떤 워드를 접근하냐에 따라 속도가 달라지는 비균일 메모리 접근(NonUniform Memory Access, NUMA) 멀티 프로세서라는 스타일이 있다. NUMA가 UMA에 비해 구현이 어렵지만, 가까운 메모리일 수록 빠르게 접근 가능하고 확장성이 크기때문에 효율적이다. 또한 동기화(Synchronization)의 문제도 중요하다. 한가지 방법은 Lock을 이용하는 것이다.




정리


각 CPU 마다 독립된 Cache공간이 존재한다. 다수의 CPU의 동작으로 독립된 Cache가 메모리와의 일관성을 지키지 못하면 Cache Miss가 늘어나고 이를 줄이기 위해 스누핑 프로토콜을 사용 하게 되는 것이다.







반응형