멀티코어로의 길

작년 10월 (그러니까 2007년 10월) MSDN 매거진을 보니 “Managed code 를 멀티 코어 머신에서 최적화하기"라는 기사가 있더라. MS의 Task Parellel Library; TPL이라는 라이브러리를 소개한 글인데 개략적인 내용은,

for (int i = 0; i < 100; i++) {
  a[i] = a[i]*a[i];
}

이런 코드 블럭이 있을 때 이런 형태를 코어 수에 맞게 managed code 레벨에서 쪼개서 실행해주겠다는 것이다. 위에 제시한 for 문의 경우 각 a[i] 들이 다른 a[j] (where i != j) 에 의존성이 없기 때문에, 동시에 실행해도 무방하다. 그래서 코어 수 만큼 쪼개서 실행하면 코어 수보다는 약간 적은 정도의 가속효과가 생기게 된다. (물론 상황에 따라선, 특히나 캐쉬 힛 여부에 따라선 나빠지지 말란 법이 없긴하지만)

사실 이런 아이디어는 intel 이 제공하는 C++ 라이브러리인 intel threading building block; intel TBB1 에서도 채용한 방식인데, managed code 쪽에서는 약간 다른 표현 양식을 쓰고 있긴하지만 다음과 같은 면에서 동일한 방향 이라고 생각된다.

프로그래머에게 _노출되는 인터페이스_가 기존 코드의 문법을 최대한 활용한다.

즉, 해당 언어 도메인 안에서 표현된다. intel TBB (이하 TBB)의 경우 for 문 자체를 parallel_for, parallel_reduce 같은 구문을 사용해서 최대한 for 문과 유사하게 표현하고 (다만 iterator 표현이 복잡?해진다), managed code의 Parallel.for(), Parallel.Aggregate() 구문 역시 for 문과 유사한 형태로 위 코드의 병렬화를 수행한다. 즉 아래와 같은 코드가,

Parallel.For(0, 100, delegate(int i) {
  a[i] = a[i]*a[i];
});

Anonymous function 을 인자로 주는 방식 – C# 문법 – 을 사용한다. C++에도 이런게 되면 좋겠는데(물론 boost::lambda를 쓸 순 있지만). intel TBB의 경우에는

parallel_for(blocked_range<size_t>(0, n), square_functor(a), auto_partitioner());

같은 코드를 사용한다. 다만 square_functor 가 특정 원소에 대한 연산이 아니라 range 에 대한 연산이어야 한다. Range는 [시작, 끝) 정도로 표현되는 두 개의 iterator를 생각하면 된다. (STL과 비슷한 표기)

그리고 조금 생각해보면 짐작할 수 있는 내용이지만, 둘 다 실제 구현부분(라이브러리를 사용하는 프로그래머가 작성할 부분)은 싱글 스레드의 로직 처럼 작성할 수 있게 되어 있다. delegate로 구현한 부분이나 square_fuctor나 실제로 구현할 때는 마치 싱글 스레드인 것처럼 짤 수 있다. 물론 각 원소에 상호 영향을 주고받아야하면 일반적인 멀티스레딩 처럼 락도 쓰고 그래야하지만;

멀티 코어 CPU의 코어 수를 _추상화_한다.

즉, 프로그래머가 일일이 CPU 코어 수를 확인하고 이에 맞춰 프로그램을 실행하거나 만들어줄 필요를 줄인다. 추가적인 for 문 문법을 쓰거나 해야하니 완전히 제거되는 것은 아니지만, 코어 수에 맞춰서 스레드를 생성하고 관리하고 동기화하고 … 하는 작업은 하지 않아도 된다2. 다만 싱글 코어 CPU에서는 약간 느려지긴 하겠다.

좀 더 _복잡한 병렬화에는 Task_라는 개념을 도입한다.

즉, CPU 코어 수를 프로그래머가 미리 알 수 없기 때문에 (광범위하게 배포해서 사용하면 절대 이건 통제되지 않는다), 라이브러리 레벨에서 최적화된 스레드 수를 사용하기 위해 스레드 위에서 실행될 수 있는 태스크 라는 좀 더 작은 단위 작업을 도입한다. 그리고 이런 작업들을 적당한 수의 스레드 위에서 + 여러 개의 코어 위에서 동작하게 해준다.

대충 이런 공통점을 MS와 intel의 라이브러리 개발자들이 공통으로 생각하고 있는게 아닌가 하고 추측해본다. 클라이언트에도 이젠 CPU 속도 향상 자체를 크게 기대하기는 힘들다. 결국 성능이 필요하면 속도를 높이는 대신 코어를 늘리는 방식으로 CPU를 발전시키고 있으니 병렬 실행으로 가야한다. 그렇지만 하드웨어를 통제할 수 없는 클라이언트 쪽에 뭔가 쉽게 멀티스레딩하긴 어려우니, 이런 약간의 추상화 계층을 두고 상대적으로 쉽게(기존 문법의 약간의 변형정도) 접근해가려는게 아닌가 한다.


  1. 얼마 전에 intel TBB에 관한 포스팅을 시작할까 했는데 너무나 호응이 없어서 쓰다 포기한 포스팅이 있다. 이제 다시 시작하게 될지도 모른다[…]. ↩︎

  2. 그렇지만 성능이 매우 중요한 일부 서버 응용의 경우엔 이걸 다 작성한다. 프로그래밍하는 것과 동시에 서버에 필요한 CPU 수, 코어 수, 메모리 용량, 네트웍 연결과 대역도 다 고려하는 경우가 많다. 물론 스펙 자체를 프로그래머/개발자 전체가 관리할 수 있다는 점에선 (예산 제약은 있지만) 오히려 다행인지도 모른다. ↩︎