최근 너무 바빠서 포스팅할 여력이 없었습니다.
개발을 아예 관두거나 그런건 아니구요 짬짬히 개발하고 있는데 생각보다 밖으로 드러나보이는 결과물을 없어서 포스팅을 못하고 있었습니다.
최근에는 엔진을 최적화하는 작업을 하고 있습니다. 이곳저곳 정보를 수집하다보니 DOD(Data Oriented Design)에 대한 이야기를 듣고, 이녀석이 멀티스레드와 캐시 적중률에 아주 좋더라! 하는 이야기를 듣고 내부 구조를 개선하는데 시간을 보내고 있어요.
DOD에 대해서 간단히 설명하자면, 일반적으로는 Object Oriented하게 프로그램을 작성하죠. 직관적이기도 하구요, 많은 프로그래머들이 이렇게 코드를 작성하도록 교육을 받았기 때문이죠. OOP의 유용성에 대해서는 다시 논할 필요는 없을것 같고, 어떤 점에서 한계가 있는지 간단하게 정리해볼게요
* 연관된 데이터가 뿔뿔히 흩어지게 됩니다. 추상화와 상속을 많이 사용하기 때문에 간단한 클래스조차도 new로 생성해서 포인터로 보관해야하죠. 그 결과 수 많은 데이터가 비슷한 구조를 가지고 있으면서도 연속되기 보다는 여기저기 뿔뿔히 흩어져서 배치됩니다. 이게 뭐가 문제냐면 포인터 쫓아가면서 참조해서 데이터를 조작하다보니 단순한 작업을 하는데에도 캐시미스가 많이 발생하고 이걸로 소중한 시간을 잡아먹게 된다는거죠. 일반적으로 RAM 접근 작업은 CPU 내에서 처리되는 작업보다 매우매우 느리기 때문에 아무리 알고리즘을 잘 짜두어도 결과적으로 프로그램이 느려지는 원인이 됩니다.
* 멀티스레드화시키기 어렵습니다. 위와 연관된 문제인데, OOP에서는 수많은 클래스들이 상속과 소속으로 복잡한 관계를 이루게 되어서, 언제 어디서 이 데이터를 참조할지가 명확하게 알수 없어집니다. 단일스레드로 대부분의 작업을 처리하던 시절에 이것은 큰 문제가 되지 않았지만, 요즘의 대세는 멀티스레드지요. 이건 멀티스레드화에 아주 큰 부담으로 다가옵니다.
그래서! Data Oriented Design에서는 뭘 어쩌자는거냐면, 그냥 관련된 데이터끼리 연속되게 묶어서 배치하자는 겁니다. 제가 작업하던 코드를 가지고 간단히 설명하자면 다음과 같습니다.
처음에는 게임 오브젝트 디자인을 다음과 같이 했습니다.
일반적인 OOP에서의 디자인
class ISObject; // 모든 게임 오브젝트들의 추상클래스
class ISObjectMovable : public ISObject; // 움직일수 있는(즉, 물리엔진의 효과를 받는) 오브젝트들의 추상 클래스
class ISObjectDrawable : public ISObjectMovable; // 보이는(즉, 렌더링될수 있는) 오브젝트들
//그리고 이 아래로 Bone 애니메이션이 적용될수 있는 오브젝트, Morph 애니메이션이 적용될수 있는 오브젝트 등등등의 자식클래스가 잔뜩있었습니다.
DOD로 바꾼 디자인
class ObjectContainer
{
// 원래 ISObject 추상 클래스에 있던 데이터들을 배열로 바꾸어 컨테이너에 담았습니다.
int data1[256];
int data2[256];
float data3[256];
...
};
class ObjectMovableContainer
{
// 원래 ISObjectMovable 추상 클래스에 있던 데이터들을 배열로 바꾸어 컨테이너에 담았습니다.
int data1[256];
int data2[256];
float data3[256];
...
};
class ObjectDrawableContainer
{
// 원래 ISObjectDrawable 추상 클래스에 있던 데이터들을 배열로 바꾸어 컨테이너에 담았습니다.
int data1[256];
int data2[256];
float data3[256];
...
};
...
class ISObject
{
// 오브젝트 데이터가 실제로 컨테이너의 몇번째에 위치해있는지를 저장해둡니다.
// ID번호 말고 아예 포인터로 저장하는것도 괜찮습니다.
size_t objectID;
size_t objectMovableID; // 움직일수 있는 오브젝트일 경우 이 값을 사용하면 되고, 아닌 경우는 -1 같은걸 넣어두면 되겠죠.
size_t objectDrawableID; // 렌더링 될수 있는 오브젝트일 경우 이 값을 사용하면 되고, 마찬가지로 아닌 경우는 -1 같은걸 넣어두면 되겠죠.
};
// 그리고 오브젝트의 처리는 ISObject클래스에서 수행하지 않습니다. 처리함수는 클래스 멤버함수로 두지 않고 전역함수나 컨테이너 클래스의 멤버함수로 옮겨둡니다.이렇게 하면 실행시에 가상함수를 통한 호출이 필요없어지겠죠.
이렇게 변경하느라 깊숙히까지 뜯어고쳐야했습니다. 꽤나 귀찮은 작업이었어요. 처음부터 이렇게 설계했으면 얼마나 좋았을까...
비슷한 삽질을 하나 더 하고 있습니다.
DDR(Data Driven Render)를 엔진에 적용하려고 하고있어요. 이건 아직 적용이 현재진행형이라 길게 말할수 있는건 없구요, 모든 렌더링 과정을 코드가 아니라 데이터에게 맡겨버리는겁니다. 즉, 엔진의 추상화 과정이라고 할 수 있는데, 이렇게되면 다양한 환경에 맞춰 유연하게 렌더링 작업을 하는게 쉬워집니다. 데이터만 갈아끼우면 다른 렌더링 작업도 할수 있으니, 엔진의 재활용성도 높아지겠죠.
[포니 게임 개발] 39. 안티앨리어싱(FXAA) 적용 (3) | 2013.12.21 |
---|---|
[포니 게임 개발] 38. 반사 매핑 구현 (2) | 2013.11.04 |
[포니 게임 개발] 37. 외곽선 그리기 완결 (7) | 2013.10.29 |
[포니 게임 개발] 36. 그래픽 품질 테스트 (0) | 2013.10.21 |
포니 게임 성능 테스트 (10) | 2013.10.05 |
[포니 게임 개발] 35. 완성도 높이기 (1) | 2013.09.15 |
댓글 영역