node.js 혹은 CPS 단상

지난 주에 둘째 태어나서 병원에 있는 동안 SNS와 블로고스피어를 스쳐지나간 글 중 제일 눈에 띄는 부분이 “CPS와 node.js” 관련된 내용이었다.

iPhone은 정리에는 그다지 좋은 기기라고 못하겠다. 내가 생각하던 문맥을 저장하기엔 내 활용 방법의 문제거나 기기가 부족하거나; 여하튼 기억나는 글들을 여기 정리하자면 (twitter는 정리가 불가능하니 제외),

웹 서버 아키텍처와 프로그래밍 모델의 진화 http://ricanet.com/new/view.php?id=blog/110903 (node.js의 I/O 모델을 점검하기에 좋은 글)

Node.js의 소개글 들에 대한 유감 http://himskim.egloos.com/3810574

이에 대한 홍민희 님의 글 http://blog.dahlia.kr/post/18300740247

그리고 다시 (홍민희 님의) 비동기 I/O 프로그래밍 모델 관련 글 http://blog.dahlia.kr/post/18355002657

 

이에 대한 내 생각을 정리하자면:

1. node.js는 hype이라 생각한다. node.js 광신도(…)들이 말하는 장점은 이미 오래된 얘기고 (이벤트 기반의 비동기 I/O 모델, 단일 이벤트 루프와 그로 인한 싱글 스레드의 단순함), 이런 시도를 하는게 node.js만 있는 것도 아니다. 다만 세상에 JavaScript 프로그래머가 많기 때문에 그렇게 느껴지는 것 뿐이지. 서버 혹은 백엔드 프로그래밍에선 너무 오래된 — (게임) 서버 프로그래밍을 주로 하던 이들 사이엔 이게 왜 이슈가 되는지 궁금해하는 사람들이 나온다 — 이슈라서 말이지.

다음 언어들로도 `흔히’ CPS 스타일의 프로그래밍을 할 수 있다. 그리고 이 언어들 모두에서 node.js에서 `하면 안된다’라고 주장하는 한 이벤트에서 긴 계산을 하는 일도 실행 모델에 따라선 해도 된다. 그리고 이 언어들에선 `흔히 하는 일’에 불과하다.

Go: goroutine 자체가 일종의 CPS 스타일 프로그래밍이다. 그리고 일반적인 이벤트 큐 처럼 쓸 수 있는 channel 개념 역시 존재한다. 그리고 이 두 개념을 최대한 활용해서 만든 언어가 Go다. 자세한 내용은 내 과거 포스팅을 참조하자 (https://rein.kr/blog/archives/tags/go) 개인적으론 이런 류의 I/O 비동기 / 멀티플렉싱을 생각하고 짠다면 Go가 가장 깔끔한 프로그램이 나온다고 생각한다.[1]

C#: C#의 익명 함수 (delegate라고 부르던데) 역시 이런 CPS 스타일로 프로그래밍 할 수 있다. Nexon의 `마비노기 영웅전’과 `마비노기 2’도 이런 개념을 활용해서 서버를 구현했다. 2011년 NDC 세션의 내용을 정리해놓은 것처럼 (https://rein.kr/blog/archives/2696https://rein.kr/blog/archives/2671) 이걸가지고도 JS에서 흔히 하듯이 성공/실패 처리를 cps로 작성한다.

C++: C++11의 lambda 혹은 그 이전의 std::function[2] 을 이용하면 역시 CPS 형태의 코드를 작성할 수 있다. 내가 작성한 게임 서버 라이브러리 역시 이를 이용한 CPS 스타일의 코드가 포함되어 있다.

 

2. node.js가 단일 스레드, 즉 단일 문맥만으로 처리하기 때문에, computation-intensive한 작업은 처리할 수 없다. 그래서 어쩔 수 없이 백그라운드 태스크 큐 등을 써야하고, 이로 인해 생기는 지연 시간(latency) 문제는 극복할 수 없다.
게임 서버에서도 node.js처럼 (혹은 그보다 낫게)

  • 복수의 스레드가 네트워크 이벤트(read/write)를 비동기로 처리하고,
  • 네트워크 이벤트를 서버 메인 스레드(그러니까 논리적으론 단일 스레드 프로그램)로 전달하고,
  • 서버 메인스레드가 내부 로직을 처리해서 다시 네트워크로 보내는

구조를 사용한 서버들이 있다. 다만 이 경우엔 계산 량이 많은 경우 지연 시간이 길어진다. 그것도 게임 상에서 상호 연관이 없어 보이는 부분들 사이에 영향을 주는 형태로 -_-;
게임 서버는 웹과는 좀 다르게 persistent storage에 다 저장하지도 못하고, 각 연결 간에 상호 작용도 많아서 어쩔 수 없이 생기는 문제이긴하지만 맨 첫 글에서 얘기하는 것 처럼 node.js가 `유니크’한 것은 아니다. JavaScript로 서버 프로그래밍을 진행한다는 것은 꽤 참신하지만 말이다.

그리고 서버 메인스레드가 1개가 아니라 복수개 일 수도 있고 — 내가 본 코드에선 이게 주류다 — 걔 중에도 IO 스레드가 full swing을 하돼 non-blocking한 알고리즘을 이용해서 복수의 큐를 사용하고, 블럭킹을 제거한 구현체도 있다.

 

3. 프로그래밍 모델은 CPS는 `쉽게 만들어낼 수 있지만’, 프로그래머에겐 꽤 골치거리라 생각한다. 홍민희 님의 말대로 가장 쉬운 형태는 co-routine일 것이라 생각한다. CPS처럼 이전 문맥에 대해 생각할 필요도 없고, 불필요한 nesting (node.js말하는 거임) 도 필요치 않다. 다만 이 경우에는 언어적인 수준에서 지원하지 않으면 안된다. 불행히도 내가 사용하는 주요 언어인 C/C++의 경우 co-routine을 사용할 수단은 OS fiber수준 정돈데, 이 경우엔 다른 동기화 객체들을 쓸 때, 동기화 단위가 스레드라서 fiber끼리 switching할 때 지옥을 보게 된다. 하지만 node.js 정도의 프로그래밍 모델에선 이게 가장 간편할 것이다. 각 연결 별 문맥끼리 상호작용이 적다면 더더욱 말이다.

  1. 이 언어를 만든 회사가 Google이란 생각을 하면 더 그렇다 []
  2. 혹은 그 이전 구현체인 std::tr1::function, 혹은 이거랑 별반 다를게 없는 boost::function을 생각하면 더 이전부터 []

프로그래머의 일상: race-condition, 글쓰기, …

Race condition

5월 말에 예비군 훈련을 다녀오고나니 (…), 내가 작성한 C++/Win32 기반의 서버 런타임에 문제가 발생했더라. 정확한 요인은 모르겠지만, 메모리 사용량이 팍 뛰었고, 32bit 프로세스의 한계로 사망 -_-

다행히 ipkn이 작성한 부하 테스트 툴로 원인을 좁힐 수 있었다. 해당 프로세스에 미리 할당하는 소켓 수 이상으로,

  1. 연결을 계속해서 맺고
  2. 과다한 트래픽을 흘려보내고,
  3. 다시 끊고

…을 수 시간 반복하면 서버가 크래시하더라.

이미 수 개월 동안의 라이브 서비스 동안 별 문제 없어서 안정화 끝났다라고 생각했는데 그게 아니었던 모양.

처음 찾은 버그는 이런 거였다. 대략 소켓 래퍼 객체의 참조 카운트를 가지고 최종적으로 할당된 자원을 재활용하는데, 단 하나의 시나리오에서 이 패킷 버퍼와 소켓의 참조 카운트 해제하는 순서가 반대(…)로 되어있더라. 그래서 대략 k 시간의 코드 리뷰 후에 잡았음. 그나마 패킷 버퍼 쪽에서 문제가 되는 코어 덤프가 있었으니 Orz.

…그리고 이 상태로 다시 부하 테스트 시작. 또 죽는다? 다행히도 디버그 빌드에서도 재현이 된다. 문제는 heap corruption이라는 것? -_-;; 정확히 1 bytes가 0로 셋팅되더라.[1] 그래서 서버 런타임 전체에서 1바이트 세팅하는 코드를 전부 뒤졌다.
그 결과, 일부 per-thread 타이머 객체가,

  • 해당 스레드에서 타이머 객체가 종료되어 자원 해제
  • 다른 스레드에서 해당 타이머 객체 invalidate

하는 경우가 있을 수 있더라. 역시나 참조 카운터 문제 -_-; 선형화처리 코드는 들어가 있지만, 해당 객체가 살아있다는 보장이 없는 상황이라, 대부분 잘 돌지만(…) 이렇게 레이스가 일어나면 얼마든지 맛이 갈 수 있더라;

이 두 가지 문제 해결하고나니 대략 (눈에 보이는) 버그는 잡은 거 같지만…
일단 부하 테스트는 잘 버티고 있다.

…근데 애초에 문제가 되었던, 갑자기 메모리 사용량 1G 점프하던건 뭐였을까? 십중팔구는 처리가 늦어지고, 그에 따라 센드 큐가 늘어나고, 그에 따라 처리가 더 늦어지고(…) 하는 악순환의 결과였던 거 같지만 -_-;

만약 크래시 덤프도 없고, 디버그 모드에서 재현 못했으면 어쨌을지를 생각하니 좀 끔찍하긴 하구나 -_-;

글쓰기

역시 공대생을 위한 글쓰기 수업을 들었어야 했나 -_-;
내가 얘기하고자 하는 부분이 아닌 걸 계속 써야하나?

예전에 책 번역 할 때도, 영어보다 한국어 실력이 더 문제란 느낌이었으니 에휴.

  1. Win32 DEBUG heap이 마킹해주는 덕에 이건 파악할 수 있었다. 정말 다행임 []

멀티코어 활용할 방법이 정말 멀티스레딩 뿐이라고?

예전에 `Multicore의 concurrency를 위해선 멀티스레딩 뿐인가?” 라는 글을 썼다.

OpenMP나 tbb, 혹은 MS의 Parallel Patterns Library 모두 일을 더 쉽게 만들어주는게 아니다. 그네들은 병렬 프로그래밍을 쉽게해주는 기능을 제공해 주긴 한다. 하지만 당연히 싱글스레드 프로그래밍보다 어렵다.
즉, 원래 목표여야 하는 “병렬 하드웨어 활용을 쉽게해주는 것”이 아니다. 이 툴들의 목적은 현재 단일 스레드로 작성한 프로그램을 , 어떻게 멀티스레드 프로그램으로 수정하면 병렬 하드웨어를 좀 더 잘 쓸 수 있을까 하는 경우를 해결하는 것이다.
이거랑 멀티코어 CPU를 잘 쓰는 일은 동일한게 아니다.

쉬운 길은 이미 “많은 프로그램들이 잘 하고 있는 일”이다.간단하게, 하드웨어가 병렬화가 되면 `할 일을 더 주면 된다’. (Gustafson’s Law)
웹 서버나, hadoop처럼 부하가 늘어나면 작업 단위를 처리하는 `프로세스를 더 많이 띄우면 된다’.복수의 프로세스(스레드)가 특정 상태를 공유하면서 / 업데이트하지 않는 이상, 이 방법은

  • 작성하기 익숙하고 (익숙한 싱글스레드 프로그램이다!)
  • 디버깅 하기 쉽고 (멀티스레드 프로그램은 비결정론적이다)
  • 기존 코드를 거의 변경하지 않아도 된다

라는 엄청난 장점이 있다.
서버 프로그래밍의 — 특히 게임 서버 프로그래밍 — 목적은, 지연 시간을 일정 이하로 유지하는 수준에서 최대 처리량(throughput)을 얻는 것에 있다고 생각한다. 즉, 개별 서버 프로세스가 비슷한 일 — 프로세스마다 1개 채널의 로비를 처리한다거나 — 을 처리해서 부하를 적당히 쪼개고, 싱글스레드에서 처리가능한 수준[1] 의 부하를 담당한다면, 굳이 이걸 omp/tbb/ppl 같은 걸 써서 복잡하게 짤 이유는 없지 않은가?

물론 공유 상태가 많으면 MT로 짤 수는 있겠지만, 그 경우에도 비슷한 원칙을 유지한다.

  • 싱글스레드화 되는 부분 — 동기화 처리부분 — 은 메시지 교환 등으로 우회하거나, 최소화하고
  • 같은 일을 하는 프로세스…대신 스레드를 여럿 둔다
  • 그리고 이 스레드의 수만 늘린다

즉, 본질적으로는 일부분을 제외하고는 싱글스레드 프로그래밍을 하고, 이 부분을 늘려서 주어진 병렬 하드웨어를 잘 쓰자라는 것.

그러니까, 싱글 스레드 성능 향상이 목적이 아니라면, omp/ppl/tbb 다 그렇게 영양가 높은 얘기는 아니다라는게 내 생각이다.[2] 오히려, 전체 구조를 잘 생각하고 똑같은게 여러 개 돌 부분을 찾고, 이 부분을 여럿 띄울 생각을 하는게 — 예를 들자면 마비노기 영웅전의 micro-kernel 틱한 구조에서 개별 서비스를 여러 개 띄우는 것 처럼 — 생산적인게 아닐까?

그 이전에(…) 서버 자체는 많은 경우 애초부터 멀티스레드로 짜고, 독립적으로 할 수 있는 부분도 많기 때문에 tbb같은게 적합할 태스크 패러럴, 혹은 데이터 패러럴한 워크로드가 아닌 것도 좀… 코어를 다 차지할 만큼의 스레드들이 있는데, 굳이 단일 스레드 프로그래밍에서 CPU  코어 여러개 쓰기 위한 방법을 가져다 쓸 이유가 있을까?

내가 리뷰 안 쓰는 무언가 때문에 이런 글을 썼다고 느끼면 “그게 다 기분 탓” 입니다.

  1. 그러니까 엄한 지연 시간이 나오지 않는 []
  2. 물론 일부분, 예를 들어 tbb scalable allocator라거나 concurrent container류는 그 자체만으로 쓸모있는 라이브러리이기 때문에 여기저기 쓸 곳이 많다. 그렇지만 parallel_for, parallel_reduce 같은게 정말 필요한가? []

병목은 어디에?

2월 말부터 시작해서, 게임 리소스를 네트워크 너머로 배포하는 유틸리티를 작성하고 있다. C# 공부를 겸해서(…) 틈틈이 짜고 있는데, 요 며칠간은 어떤 골치거리 에 매여있는 터라[1] 주로 성능 평가만 했는데, 병목현상이 생기는 곳이 좀 예상치 못한 곳이더라.

개념적으로 보내는 쪽(이하 S), 받는 쪽(이하 R) 두 개의 entity로 구성되며, 대략 다음과 같이 동작한다

  1. S에서 보낼 디렉터리 전체의 요약 정보 — 이름 크기 md5 hash — 를 담은 Merkle tree를 만든다.
  2. R 쪽에서도 이걸 구하고 이 tree를 비교해서 어떤 파일이 없고, 어떤 파일이 다른지 판단한다.
  3. R에서 없는 파일의 전체, 혹은 다른 파일의 일부를 요구한다.
  4. S에서 이런 파일의 전체/일부를 전송한다
  5. R과 S에서 전체 디렉터리의 Merkle tree를 다시 구한다; 단 이번에는 다른 hash 함수를 쓴다. 그리고 이 root node만 전송한다.
  6. 이걸 비교해서 전체 파일 전송이 성공했는지 판단한다.

처음 짠 코드에서 병목은 3,4 였고, 이 부분을 .net framework의 TPL을 이용해서 복수 request / 복수 sendfile 형식으로 처리하는 식으로 디스크 / 네트워크 사용률을 높여서 해결했다. 근데 오늘 결과를 측정해보니, 예상외의 장소에서 병목이 나오더라.

3.2 GiB / 1700 파일 정도인 모 프로그램의 설치 디스크를 통째로 전송해봤는데, 대략 11분이 걸렸다. S, R에서 각각 Merkle tree를 계산하는 게 약 40초[2] 걸린다. 다음은 전송(3, 4)이 약 5분 걸리더라. 그리고 마지막으로 5단계에서 양쪽에 각각 6분 정도(……….)의 시간이 걸리더라. 16bytes hash를 생성하는 MD5대신에 좀 더 큰 48 bytes (384bits) hash를 생성하는 SHA384를 썼는데 시간이 대략 Orz.

SHA384가 실제로는 SHA 512의 truncated 버전이라, 좀 느릴 거라 생각하긴 했는데, 생각보다 너무 느리다. 거의 똑같이 계산하는데 MD5는 1분도 안 걸리고, SHA384(512?)는 6분이 걸리다니 Orz.

다른 해시를 쓰거나, Merkle tree를 계산할 때 병렬로 – 아마 work-stealing queue를 만들고, I/O 와 CPU 계산에 들어가는걸 고려한 스레드 수를 써서 – 계산해야 하려나?

  1. 이전에 쓴 글을 보면 짐작하리라 []
  2. 물론 이건 양쪽 머신에서 동시에 진행 []

리뷰: 프로그래머가 몰랐던 멀티코어 CPU 이야기

art.oriented의 김민장(object)님이 쓴 책이다. 한빛미디어의 Blog2Book 씨리즈 중 하나.

평소에 저 블로그를 구독하고 있다면, object 님이 전반적인 컴퓨터 구조와 최적화에 관해서 얘기하는 걸 여러 번 봤을 거다 – 구독하지 않고 있다면 RSS 리더에 추가하는 걸 추천! 잘 읽히는 블로그 글 솜씨만큼이나 책도 쉽게 읽을 수 있어서 좋았음.

이 책에서는 컴퓨터 구조, 특히 CPU 에서 명령어를 빨리 실행하기 위해 무엇을 하는지를 중심으로 현대 CPU의 여러 부분을 설명하고 있다. 간단히 읊어 보자면,

  • 프로세서의 각 부분에 대한 설명
  • 컴퓨터 성능 평가와 암달의 법칙
  • 고성능 프로세서에 들어간 최적화 부분: 파이프라인, 비순차 실행, 하이퍼 스레딩, 멀티 코어 그리고 데이터 병렬성
  • 분기문의 복잡함과 최적화: if 문, virtual function
  • 멀티코어 CPU 혹은 SMP 아키텍쳐에서 벌어지는 몇 가지 문제들
  • 느려터진 메모리의 최적화: 캐시와 메모리 계층 구조, 메모리 프리펫치

를 비롯해서, 이 포스팅에선 언급 안한 것도 잔뜩 포함해서, 컴퓨터 구조의 여러 내용을 설명하고 있다. 특히 상대적으로 쉬운 내용 – 프로세서의 나 각 실행/제어 단위에 대한 설명, 기본 개념에 대한 설명 – 부터 시작해서,  비순차 실행이라거나 분기 예측, 투기적 실행(speculative execution)처럼 학부 수준의 컴퓨터 구조 시간에는 간단히만 다루는 현대의 복잡한 CPU 구조의 부분부분을 설명해주기 때문에, 좀 어려울 수는 있어도 따라갈 수 없는 내용은 아니다.

물론 이 주제의 상당 부분은 학부 컴퓨터 구조와 컴파일러 시간에 배우는 내용보다 좀 더 최신이고, 상대적으로 좀 더 깊이 들어간 부분이 있기 때문에, 빈말로도 “쉽다”라곤 말 못하겠다. 그렇지만 학부 수업을 차근차근 듣는 기분으로, 찬찬히 살펴본다면 얻을게 정말 많다.

하드웨어 얘기이긴 하지만, 이 하드웨어가 우리가 만드는 소프트웨어와 직접적으로 연관이 있고, 하드웨어의 많은 부분은 소프트웨어 쪽 구현에도 영향을 준다. 책에서도 몇 번 강조하는 얘기지만, 여기서 설명하는 파이프라인의 개념은 여러 멀티스레드 프로그래밍에 나오는 개념이다(Task-parallel pattern; 잘하면 task+data parallel pattern도 됨). 비슷하게 메모리 미리 읽기(pre-fetch)나 캐시 활용 같은게 실제 응용 프로그램에서 쓰이는 사례는 말 안 해도 알 것이다. 그래서 “난 SW에만 관심이 있어!”라고 말하는 사람(=rein)들에게도 좋은 책이라고 생각한다.

비록 오자가 좀(…) 있다곤 하지만, 빠르게 업데이트 되고 있어서, 오자 페이지를 찾아가서 확인하면서 보면 괜찮을 거다;

rein은 이 책을 그레이트 코드 2권 같은 책에 매우 큰 흥미를 보였거나(…), 컴퓨터 구조에 대해서 알고 싶거나, 혹은 더 배우고 싶은 분들에게 매우매우 추천한다. 비슷한 이유로 팀 신입 사원들에게도 읽어보라고 권하고 있다 :D

Sun Compiler 에서 Transactional Memory 지원 시작

(실험적이지만) Sun의 Sun Studio C++ 컴파일러에 트랜잭션 메모리(transactional memory) 기능을 지원하기 시작했다고 TM & Languages 그룹에 새 글이 올라왔다.

인용하면,

Daniel Nussbaum – Sun Microsystems – Burlington United States

Sun Microsystems is happy to announce experimental support for
Transactional Memory in C++.  The C++ compiler with Transactional
Memory support is based on the Sun[TM] Studio 12 Update 1 C++
compiler.  This experimental release is available for SPARC-based
systems only

란다.

실험적인 지원이고 – 즉 production code엔 쓰지 말란 소리겠지? – 구현 자체는 (역시) Sun 에서 만든 SkyTM 이라는 하이브리드 TM에 기반을 두고 있다고 한다.

TM 자체가 concurrent programming에서 silver-bullet은 아니겠지만 어느 정도 크기의 break-thorugh이고, 실제로 몇 개 회사에서 C++ 위에[1] TM 구현에 관해 논하는 시점이 되었으니, 얼마 후면 일반적인 concurrent programming에서는 explicit locking을 보는 일이 꽤 많이 줄어들게 될지도 모른다.

…다만 갈 길이 좀 멀긴하지만 /먼산

  1. 정확히는 C++0x []

리뷰: Clean Code

twentyeleven

여러가지 의미에서 Kent Beck의 Implementation Patterns 과 비슷한 느낌의 책이다.

우리가 다루는 프로그래밍 추상화 단계 – 각 단위의 이름(변수나 함수, 클래스 등등…), 실행문(statement), 함수, 클래스, 시스템 레벨에서 어떻게 하면 “쉽게 읽고, 이해하고, 변경할 수 있는” 코드를 짤 수 있는지 설명한다. 그리고 comment나 클래스 설계 원칙에 대한 일반론들(open-closed principle, single responsibility principle…) 역시 다룬다. 사실 일종의 best-practice 북인 셈?

하지만 단순한 이런 류의 책에서 더 나아가서 – 개인적으론 이 부분이 Kent Beck의 Implementation Pattenrs 보다 훨씬 나은 점인 것 같다; 사실 TDD by examples 같은 느낌이기도 하다 – 실제 코드 (JUnit에서 따온 것도 있고, SerialiDate 클래스 같은 것도 있고 … ) 를 점진적으로 개선하는 실제 작업 내용을 한 단계씩 차근차근 밟아가면서 비교해주는 점이 좋았다. “나쁜 ???”에 해당하는 예를 적절히 보여준다는 것도 장점. 특히나 이것들을 “실제로 동작하는 프로그램”에서 가져온다는 점이 아주 좋음…

병행성(concurrency)에 대해서 다루는 부분도 읽을만 했다 – 물론 단점이 꽤 있다. 특히 처음 인용한,

“Objects are abstractions of processing. Threads are abstractions of schedule”

은 꽤나 맘에 듬.

곧 번역서가 나올 것이란 얘기도 있으니, 발간되고 나면 한 번 읽어보는 것도 괜찮겠다.

 

하지만 이 책에 단점이 없는 것은 아니다. 일단, 이건 개인적인거지만 Java 에 완전히 종속적인 부분이 나오며, 다른 언어에 대한 추가 고려는 없다(…). 그리고 첫 장에서 저자가 설명하듯이,

“모든 내용에 대해서 독자들이 동의할 거라곤 생각하지 않는다.”

에 해당하는게 꽤 보였다. 사실 그게 크게 작용한 것은 병행성 부분. 13 장과 부록에서 크게 다루고 있는데, 저자가 너무 지엽적인데 목매달고 있으며, library와 잘 알려진 concurrent data-structure/model을 쓰라고 하면서도, 정작 중요한(적어도 현재는) Java Executor Services에 대해선 너무 짧게 다룬다. Producer-consumer나 reader-writer 문제 같이 OS 시간에 다뤄서 충분할 내용들을 길게 다루고, 현재 OS나 platform 단에서 잘 정의해서 쓸 수있게 만든 executor service 나 Future construct에 해당하는 녀석들은 제대로 다뤄주지도 않는다.

실제로 여기서 설명하는 것들 – 공유 변수의 ++ 문제 – 같은 건 그냥 병행성 입문 교재에서 다루는 정도도 충분하다. 이 책의 다른 부분들 – 정도의 차이가 꽤 크긴 하지만 – 수준으로 다뤄졌어야 한다. 특히 병행성이 어떻게 구현될 수 있는지 – data parallel, task parallel … – 같은 부분은 거의 생략하면서, 실행 순서 문제나, lock 의 가벼운 정도(Java 5의 AtomicInteger vs. synchronized construct) 주제는 너무 길게 다루고, 큰 의미도 없다. 사실 더 중요한 캐시 친화적인 프로그래밍 같은건 싹 빠졌음 Orz.

실제로 synchronized 대신에 Atomic???  류를 쓰는 것이 좋긴 하다. 그렇지만 더 중요한 것은 lock 의 세밀함(coarse-grained vs. fine grained)이나, lock 자체를 피해갈 수 있는 전반적인 프레임웍 문제지, 지엽적인 주제에 집착하는 것은 그닥 좋지 않다. 이 부분의 저자가 컨설턴트인데 이러는 것도 좀 그렇긴하다(…).

차라리 이 부분은 적당히 건너뛰고, Art of concurrency나 Patterns for Parallel Programming, 혹은 Java Concurrency in Practice 같은 책을 보는게 이 항목에 대해선 좋은 선택인 듯 싶다.

총평: 일부 동의할 수 없는 내용이나, 미진한 내용(개인적으론 concurrency 부분의 두 챕터)이 있긴하지만, 전반적으로 괜찮은 책이니, 시간이 되면 한 번 읽어보시라.

잡상: many-core 시대의 프로그래밍

이미 한참 된 글이지만, 얼마 전에 DDJ에 H. Sutter가 Design for Manycore Systems란 글을 썼다.

개괄에 해당하는 내용을 아주 간단히 요약하자면,

  • 요즈음의 시스템에서 가장 큰 어려움은 CPU와 메모리의 속도 차이고
  • 이를 극복하기 위해서 CPU 벤더들이 접근한 방식은 더 복잡한 코어를 만들어내거나
  • 더 많은 코어를 제공해서
  • CPU <-> 메모리 사이의 속도차이를 “보이지 않게” 만드는 것

그리고 나선 SUN 나이아가라 CPU의 변화등을 예로 들면서, 현재까지 (클락 속도를 올리기 위해) 취해왔던 “더 복잡한 코어”를 만들어서 메모리 접근 시간을 “프로그래머에게 보이지 않게” 하는 일은 이미 한계에 다달았고 — 이미 프로그래머에게 transparent하지 않은 방법: OOE 같은 것이 나타났고, 이는 병렬 프로그래밍에서 꽤나 골치 아픈 일이다 — 이에 따라 병렬 프로그램을 쉽게 짜고 + 메모리 접근 속도를 할 적당한 방법은

  • 더 간단한 코어를 만들고 (OOE 같은 복잡한 기능이 들어가기 이전 수준으로)
  • 이런 간단한 코어는 더 적은 수의 칩 면적(=더 적은 수의 트랜지스터들)을 차지해서 여러 개를 집어넣으면 되고
  • 이를 써서 메모리 병목에도 이 코어의 파이프라인을 충분히 활용할 수준의 하드웨어 스레드를 가능케하고 (=하이퍼 스레딩)
  • 이런 프로그래밍 모델을 써서 편하게(?) 멀티스레드 프로그래밍을 해보자

이라는 것. 그리고 이건 부수적으로 “빠른 FP 연산”과 “적은 파워 소모”라는 장점도 갖는다고 덧붙인다.

물론 이 경우는 당연히 “현재 응용 프로그램들 중 일부는 전혀 혜택을 보지 못하고, 일부는 심지어 느려진다”라는 문제가 있기에 중간 단계로는 현재의 복잡한 코어와 단순한 코어 여러 개가 들어가있는 CPU 팩키지가 될 거라고 말한다.

근데 저 아이디어에는 문제가 좀 있다(?). 실제로 저런 CPU가 현존하고 있다. PS3에 들어가있는 Cell 프로세서의 경우, 서터가 말한 것과 상당히 유사한 형태로 만들어져 있다. 연산처리량이 많고 복잡한 1 개의 Power 프로세서와, 이를 보조하는 8개의 보조 프로세서(SPE)가 있는데, 이 작은 보조 프로세서는 메모리 접근에 제약이 있고, 복잡한 명령어가 없다. 이런 복잡한 구조 때문인지 실제로 이 SPE를 사용하는 알고리즘은 구글링 해봐야 Power.org 에서 일부가 잡힐 뿐이다. 물론 메모리 접근에 제약이 있는 점 때문인게 제일 클 것 같지만[1] , 프로그래머가 이를 쉽게 사용할 수 있는 추상화 계층이 없으면 없느니만 못한 게 될 것이다.[2] .

그래서인지 이번에 나오는 MacOSX의 새 버젼(MacOSX 10.6; Snow Leopard)에는 이런 단순한 프로세서 여러 개가 들어있는 형태인 GPU를 활용할 방안으로 OpenCL이란 언어 확장 기능을 내놓았다. MS 쪽에서도 DX 11에 GPGPU를 위한 확장을 내놓으려 하고 있다. 이런 추상화가 서터가 말하는 중간 형태의 heterogenous processor를 잘 감싸준다면 성공적으로 자리잡을 것이고, 그렇지 않다면 아마도 상당 시간이 흐른 후에야 (=간단한 코어로도 현재 프로그램들이 성능이 나올 수준까지 접근한 후에야 혹은 현재 프로그램들이 concurrent 하게 바뀐 후에야) 이런 CPU 들이 충분히 팔리지 않을까 싶다[3] .

  1. SPE는 메인 메모리에 접근할 수 없으며 instruction + data를 담는 core-local한 메모리가 존재 한다 []
  2. 하다못해 보조 프로세서들을 전부 포기하고 복잡한 코어 2개인게 나을 수 있다. 실제로 Xbox360 같은 경우엔 복잡한 프로세서 3개가 한 팩키지에 들어가 있는 형태이며, 각각이 하이퍼스레딩을 수행해서 6개의 하드웨어 스레드가 동작하게 설계되었다. 그래서 상대적으로 단순한 프로그래밍 모델을 가지며, 이 때문인지 일반적인 응용에서는 성능 차이가 거의 없다 설계상으론 PS3의 Cell CPU가 나은면이 매우 많지만, 프로그래밍 모델의 복잡함이 이를 전부 상쇄해서인지 실제 업계 내의 얘기를 보면 그거나 그거나 성능은 비슷하단 소리가 많다 []
  3. 물론 시장 접근 방식이 다른 게임 콘솔 같은 거야 좀 다르겠지만 말이다 []