꽃미남 프로그래머 김포프가 창립한 탑 프로그래머 양성 교육 기관 POCU 아카데미 오픈!
절찬리에 수강생 모집 중!
프로그래밍 언어 입문서가 아닌 프로그래밍 기초 개념 입문서
문과생, 비전공자를 위한 프로그래밍 입문책입니다.
jobGuid 꽃미남 프로그래머 "Pope Kim"님의 이론이나 수학에 치우치지 않고 실무에 곧바로 쓸 수 있는 실용적인 셰이더 프로그래밍 입문서 #겁나친절 jobGuid "1판의내용"에 "새로바뀐북미게임업계분위기"와 "비자관련정보", "1판을 기반으로 북미취업에 성공하신 분들의 생생한 경험담"을 담았습니다.
Posted by 친절한티스

벌써 두번째 연재네요. 재미없는 첫 번째 글에 많은 관심을 가져주신 분들께 감사드립니다. 여러 네임드 능력자분들과 같이 글을 쓰려니 괜히 쩌렙 인증하는 거 같고, 많이 후달립니다.

앞으로 올릴 글은 제가 최근 관심을 갖고 있는 주제이기도 한 병렬 프로그래밍에 대해 써볼까합니다. 분량이 좀 많다보니 연재식으로 될거 같습니다. 그리고 창희님과 번갈아가며 쓰게 될지도... ( 일단 떠밀어보기 )


바야흐로 세상은 대 멀티코어 시대
골드 D 인텔이 멀티코어로 세상을 평정하고 프로그래머들이 병렬 프로그래밍의 세계로 뛰어들어 세상은 대 멀티코어 시대에 돌입하였다!! ... 라는 되도 않는 드립을 쳐보려고 했지만 재미없네요.

병렬 프로그래밍을 찾아 떠나는 프로그래머 해적들

이제는 흔하다 못해 스마트폰에서까지도 멀티코어를 쓰는 시대입니다. 그에 맞춰 여기저기서 병렬 프로그래밍 얘기들이 흘러나오고 왠지 나도 병렬 프로그래밍 안하면 안될거 같은 기분이 마구마구 듭니다. 게다가 게임도 갈수록 고사양화 되고 있고 싱글 코어 활용만으로는 도저히 퍼포먼스도 안나옵니다. 멋지게 게임 만들어놨는데 CPU 사용률이 25%만 올라가있으면 왠지 손해보는 느낌까지 듭니다.

싱글 코어로 공짜밥 먹던 시절
멀티코어 시대 이전에 병렬 프로그래밍이 없었던 것은 아닙니다. 백그라운드 로딩이라든가 네트웤 통신에 많이 사용되고 있었고, 지금도 사용 되고 있습니다. 하지만 메인인 게임 로직부분은 거의 직렬 방식이었습니다. 과거에는 이 것 만으로도 충분했습니다. 게다가 CPU 제조사에서 속도를 업그레이드 해줄때 마다 자연스레 게임 퍼포먼스도 올라 갔습니다( CPU 제조사에 감사합니다~ ).

그러나 CPU 제조사의 이런저런 불편한 진실로 인해 속도 업은 미비해주고, 대신 코어 수로 떼우기 시작하게 됩니다. 게임은 갈수록 고사양이 되가는데 CPU 속도 업은 거의 없어지니 게임이 느려지기 시작합니다 ( 게임만이 아니지만 우린 게임 프로그래머이니 게임이라고 합시다 ) . 이를 두고 향간에서는 더 이상 공짜 점심은 없다!! ...라고 말하기 시작합니다.

누가 내 점심을 없앴는가?

멀티 코어 어떻게 써요? 
멀티 코어를 활용해야겠다고 다짐 했습니다. 어떻게 해야 멀티 코어 CPU를 활용할수 있을까? 옵션하나 탁~ 켜주면 내 프로그램이 멀티 코어로 작동했으면 좋겠습니다. ( 실제 멀티 코어 CPU가 어떤식으로 동작하는지 알고 싶으신 분은 [김민장님 저서인 프로그래머가 몰랐던 멀티코어 CPU 이야기] 한번 읽어보시기 바랍니다. ) 하지만 현실은 그렇게 쉽지 않습니다. 내가 만든 프로그램이 멀티 코어로 동작하기 위해서는, 병렬적인 프로그램을 만들어야지만, 멀티 코어 환경에서 병렬로 작동 됩니다.

말 장난 같지만 사실 입니다...


병렬 프로그램 어떻게 만들어요? 
멀티 코어 CPU의 경우 프로그램이 실행될때 병렬성을 체크해서 각 코어에 일을 할당 해줍니다. 만약 그런게 전혀 없다. 그러면 코어 하나에만 작업이 할당되서 프로그램이 작동하게 되는 것이죠. 몇몇 게임을 실행할때 4코어인데도 불구하고 CPU 사용률이 25%만 올라가는 이유가 바로 이 것 입니다. 그렇다면 어떻게 병렬성을 높일수 있을까요? 대표적인게 바로 멀티 스레딩입니다. 여러개의 스레드가 생성된 프로그램이라면 1번 코어에서 1번 스레드를, 2번 코어에서 2번 스레드 처리하는 식으로 병렬 처리를 하게 됩니다.

이것이 병렬 처리다!!

스레드만 추가 생성하면 끝? 
위에 적은 데로라면 스레드만 추가하면 CPU에서 알아서 병렬 처리를 해주겠네요. 스레드를 마구마구 생성해서 프로그램을 만들어봅니다. 아마 조만간 ㅅㅂ... 소리가 절로 나올겁니다. 정상 작동하는게 하나도 없어 보입니다. 왜그럴까요?

멀티 스레드를 이용한 프로그래밍을 할 때에는 많은 주의 사항이 있습니다. 대표적인 문제 중 하나로 꼽히는 것이 데이터 레이싱 입니다. 일반적으로 멀티 스레드는 동일 메모리 공간에서 작동합니다. 즉, 여러 스레드가 하나의 데이터에 동시 접근이 가능하다는 이야기입니다. 그렇게 되면 여러 스레드가 동일한 데이터를 읽고, 쓰다가 원치 않는 값  변조가 일어나게 됩니다. 밑의 그림은 그 상황 예시입니다.

 
1번과 2번 스레드가 x값을 참조하고, 값을 바꿔 써넣는 코드입니다. 1번과 2번 스레드는 비동기로 동작하기 때문에 어떤 스레드가 먼저 x값을 참조하고 값을 바꿀지 모릅니다. 그래서 x의 최종 결과 값이 #1, #2, #3중 어느 결과로 나올지 알 수 없습니다.

문제는 이것만이 아닙니다. 데드락 문제도 있고, 코어 수만큼 스레드를 어떻게 분배할 것인지도 문제고, 생성된 스레드들은 또 어떻게 관리 할지도 문제입니다. 병렬 프로그래밍을 해보려다 그 앞에 산재해 있는 문제들때문에 제대로 해보지도 못하고 포기하게 생겼습니다.

쉬운 방법 없을까? 
좀더 쉬운 방법이 없을까? 눈을 돌려봅니다. 세상에는 똑똑한 분들이 많습니다. 멀티 코어 CPU가 발전하는 만큼 이를 쉽게 사용할 수 있게 해주는 라이브러리들도 발전하고 있습니다. OpenMP, TBB, PPL등이 그것입니다. 이들 라이브러리를 이용하면 직접 스레드를 관리하거나 분배하는 수고 없이 수월하게 병렬 처리를 할수 있도록 도와 줍니다.

간단하게 배열에 있는 수를 더하는 로직을 병렬로 처리 해보도록 하겠습니다. 직렬 처리 방식으로 로직을 구성 한다면 이런 모양 일겁니다.
for( int i = 0; i < 5000; ++i )
	fNumSum += fNum[ i ];
참으로 단순한 로직입니다. 5000개의 배열을 가지고 있는 fNum의 임의수들을 전부 다 더하는겁니다. 이걸 병렬로 처리 한다고 생각해봅니다. 그전에 위에서 얘기한 멀티 스레드의 문제점을 다시 떠올려봅니다. 그냥 스레드를 여러개 생성해서 위의 로직을 수행한다고 하면, 데이터 레이싱이 일어나 엉뚱한 합산 값이 나올겁니다. 이를 방지하려면 스레드에 맞게 배열을 분배 해주어야 합니다. 스레드를 2개 생성 한다면 1번 스레드는 0~2499, 2번 스레드는 2500~4999 식으로 말이죠. 그리고 작업을 수행 후 서로 합산 값을 더하면 원하는 결과가 나오게됩니다.

그런데 이것을 직접 수동으로 한다면, 스레드도 직접 생성해주고, 모든 스레드가 작업을 수행할때까지 대기했다가 작업이 끝나면 Join 시켜주고, 값 합산 해야하고, 만약 CPU 코어 수에 맞게 스레드 생성도 해주겠다 하면... 손이 가는게 이만 저만이 아닙니다. 단순한 합산 로직 하나 만드려고 들이는 정성이 배 보다 배꼽이 더 큰격입니다.

정말 이런 짓을 매번 하라고??

그럼 TBB가 출동 한다면 어떨까요??
// 병렬 처리를 위한 바디 클래스
// TBB 쓰면 그냥 이런 식으로 구성된다라는 것을 알리기 위함이므로 코드 설명은 생략
class CFoo 
{
private:
	float* m_fElement;

public:
	float m_fSum;
	void operator()( const blocked_range<size_t>& r )
	{
		size_t end = r.end();
		for( size_t i = r.begin(); i != end; ++i )
			m_fSum += m_fElement[ i ];
	}
	CFoo( CFoo& pSplitFoo, split ) : m_fElement( pSplitFoo.m_fElement ), m_fSum( 0 ) {}
	CFoo( int a[] ) : m_fElement( a ), m_fSum( 0 ) {}
	void join( const CFoo& pSplitFoo )
	{
		m_fSum += pSplitFoo.m_fSum;
	}
};

// TBB의 parallel_reduce를 이용해 fNum의 5000개 배열의 값들을 합산
CFoo foo(fNum);
parallel_reduce( blocked_range<size_t>( 0, 5000 ), foo );
코드 어디를 봐도 스레드를 생성하고, 작업을 분배 해주는 곳이 없습니다. 다~ 자동입니다. 알아서 코어수 만큼 스레드를 할당해주고, 각 스레드에 알맞은 작업량을 할당해줍니다. 작업자는 스레드 관리에 대해 전혀 신경쓸게 없습니다. 완전 좋죠~!! 짱입니다!! ( 물론 세세한 옵션등이 있지만 일단은 신경끕니다 ).

병렬 프로그래밍. 참 쉽죠?
TBB를 사용하니 병렬 프로그래밍이 엄청 쉬워진 것 같습니다. 하지만 안쉽습니다. 절대 안쉬워요. 조금이라도 병렬 프로그래밍 해보신 분이라면... 이거 참 병맛이다. 라고 말씀하실 겁니다. 아무리 툴이 좋아지고, 라이브러리가 좋아졌어도, 그걸 사용하는 사람은 기본적으로 직렬적으로 생각합니다. 동시에 처리될 부분을 모두 염두해가며 프로그래밍을 할 수 있는 사람은 극히 드뭅니다. 왜 난 이런 것도 못하지? 자책하지 마세요. 그게 정상입니다. 할수 있는 사람들이 특이한겁니다.

안 쉬워요.

To Be Continued....
다음 강에는 게임 프로그래밍에 적용하는 병렬 프로그래밍에 대해 좀더 다뤄볼게욤~
아마도... 

댓글을 달아 주세요

  1. 이전 댓글 더보기