목차 성능과 최적화. 메모리할당. STL 알고리즘. 책의 성능 단원과 다른 단원들을 함께 포괄적으로 발표를 진행 하겠습니다. 고려해볼 요인들. 메모리할당. 정적할당과 동적할당. 힙의 특징. new / delete STL 알고리즘. 함수 객체. 알고리즘. 할당자. 책의 성능 단원과 다른 단원들을 함께 포괄적으로 발표를 진행 하겠습니다. 우선은 성능부분에 있어 큰 이슈가되는 메모리부분을 간단하게 알아보고, 그밖에 게임에서 사용될수도 있는 성능에 영향을 미치는 stl알고리즘도 함께 알아보겠습니다. 그리고 먼저알아본 내용들과 성능상의 타협점을 찾아보고, 그밖의 성능에 영향을 주는 다른 요소들을 알아보겠습니다.
메모리 할당 ※ 정적할당과 동적할당. 우리가 성능과 관련해 알아볼 부분은 동적할당이다. 정적할당은 기본 내장타입(int, float 등) 혹은 독립적으로 하 나만 존재하고 변하지 않을 객체에 사용할 수 있다. : 동적할당에 따르는 성능저하가 없다. 하지만 일반적으로 게임을 개발하는데 있어서는 정적할당 보다는 동적할당이 유용하다. : 현재의 게임들은 대부분 개체들의 상호작용(다형성)을 강화하는 방향으로 진화하고있기 때문이다. 포인터를 사용해 프로젝트의 유연성을 높일수가 있다. 정적할당 int a; CTest t; 우리가 성능과 관련해 알아볼 부분은 동적할당이다. int* a = new int; CTest* t = new Ctest(); delete a; delete t; 동적할당
힙의 특징 잦은 할당/해제. 동적으로 메모리가 할당되는 공간. 스택과 달리 임의의 순서(new/delete 순서)로 메모리 할당과 해제가 이루어진다. 이 부분에서 메모리 단편화, 주인 없는 포인터에따른 메모리 누수의 문제가 발생. 성능에 있어서 문제가 된다. : 메모리를 요청한 시점부터 정확한 양의 메모리가 리턴된 시점사이에 걸리는 시간. 잦은 할당과 해제시 CPU점유율이 높아져 다른 부분(AI, 물리갱신, 충돌 검사, 네트워크 갱신, 렌더링 등)에 처리 시간이 지연된다. <메모리 누수> <단편화가 일어난 메모리> 잦은 할당/해제. 또한 가상메모리가 바닥날경우 하드드라이브로 스와핑하기위해 많은 시간이 할애된다. 동적메모리 할당의 성능은 CPU가 좋다고 해결될수는 없다. CPU가 발전할수록 메모리 역시 늘어남으로 힙관리 또한 복잡해지기 때문이다. 여기서 문제 해결방안 하나 말함. - 메모리 관리자와 메모리 풀을 이용한다. (생성과 삭제의 책임을 관리자에게 맡게 공유되는 객체나 독립객체별로 사용이 끝나면 완전히 삭제되게 만들수 있고, 풀의 메모리를 특정한 블록단위로 나눠 메모리를 할당해 줄수 있다. 이 블록은 프로그램이 끝날때 해제되는 것이므로 실행중에 단편화를 발생시키지 않는다.
new / delete CExample* ex = new CExample(); 전역 연산자. 클래스 한정 연산자. 클래스가 new / delete 를 오버라이드한다. 이 오버라이드한 연산자 내에서 힙영역에서 메모리를 할당받는 알고리즘 등을 클래스 마다 다르게 만들 수 있다. CExample* ex = new CExample(); void* operator new호출 CExample* ex = __new( sizeof(CExample) ); ex->CExample(); CExample 생성자호출 그럼 이번에는 힙을 사용할수있게해주는 new/delete에 대해 알아보자. Operator new 내부에서 클래스 크기만큼 malloc함수를 호출한다 delete도 new와 비슷한 과정을 거친다. 다만 소멸자와 free를 이용한다. 전역연산자를 재정의 하게되면 사용될수있는 다른 전역연산자 역시 재정의 해줘야한다. 메모리 풀에 맞게 연산자를 재정의 해줘야한다.
STL 알고리즘 함수객체. functor는 함수로 볼 수 있는 모든것을 가리킨다. C++함수 포인터의 문제 : 코드가 복잡해지며 템플릿 함수 포인터의 경우 typedef를 지원 하지 않는다. Functor. 함수, 함수 포인터뿐만아니라 클래스까지 함수로 볼 수 있는 모든 것을 가리킬수 있다. 즉, 다른 데이터 및 추가적인 함수(operator())와 연결되어 있을 경우, 이들은 모두 functor로 받아들일 수 있다. Template < typename T > class EvenNumbersFirst { public : bool operator() ( T a, T b ) const { return ( fabs(a) < fabs(b) ); } }; sort( vector.begin(), vector.end(), EvenNumbersFirst<int> ); functor는 함수로 볼 수 있는 모든것을 가리킨다. 때문에 함수, 함수 포인터 뿐만아니라 operator()(형변환 연산자)를 포함하는 클래스도 functor에 포함될수 있다.
Functor 어댑터 멤버함수의 경우 암시적으로 컴파일러에 의해 객체의 포인터가 매개변수로 전달된다. 전역함수, 멤버함수를 함수 객체로 사용할 수 있게 바꿔주는 클래스 객체이다. bool타입에 대한 not1, not2와 unary_function, binary_function. mem_fun 포인터를 통해 멤버함수에 접근. mem_fun_ref 객체나 참조를 통해 멤버함수에 접근. ptr_fun 함수 포인터를 통해 전역함수에 접근. bool IsOdd( int num ) { return num%2 == 1; } 멤버함수의 경우 암시적으로 컴파일러에 의해 객체의 포인터가 매개변수로 전달된다. 이로인해 함수객체로 함수의포인터만 전달할 수는 없다. 이를 함수객체어뎁터로 해결할수있다. - 내생각 다형적인 측면에서 많은 공부가 되었다. 클래스, 함수 등 함수로 볼 수 있는 모든것을 함수객체로 만들수 있다는 점이 다형성, 형변환을 이용한것같다. 어댑터를 이용하면 게임상의 자주사용되는 알고리즘들을 하나의 클래스로 캡슐화하여 게임상의 알고리즘적인 부분과 로직을 분리할수 있을것 같다. not1( IsOdd ) // error not1( ptr_fun(IsOdd) ); // success
알고리즘 비 변형 알고리즘 find / find_if, count / count_if, for_each, adjaccent_find, mismatch, equal, search 변형 알고리즘 copy / copy_backward, swap_ranges, remove(메모리제거안함) / remove_if, erase(메모리제거), reverse, rotate, random_shuffle, transform, replace, fill, generate, unique, partition 정렬 알고리즘 sort, stable_sort, partial_sort, 시퀀스병합, 정렬확인 숫자관련 알고리즘 accumulate, partial_sum, adjaccent_differnce, inner_product
할당자 STL의 메모리 관리방식을 자신이 만든 할당방식으로 바꿀수 있다. : 다양한 플랫폼에서의 메모리 관리 방식의 차이로 인한 비호환성을 해결할 수 있다. 하지만 다음과 같은 이유로 인해 현재 STL 할당자를 직접 작성하는 것은 회의적이다. 1. STL 할당자는 STL 자체에서의 효율성 저하 문제 때문에, 애써 STL 할당자 를 만들어도, 모든 플랫폼에서 동작한다거나, 기대한만큼 성능을 내주리라 보 장할 수 없다. 2. C++0x에서의 rvalue reference 도입으로 인해 STL 할당자가 활약할 영역이 더욱 줄어들었다. 3. 그럼에도 기본 STL 할당자를 교체하고 싶다면, 먼저 부스트 풀 라이브러리 (boost pool library)를 고려하라. - 내생각 할당자를 만들어야 하는 상황이라면 이때에도 메모리풀의 메모리에서 할당이 되게 할당자를 구현해야 메모리관련 문제발생이 줄어들것이다. 또한 할당자 내부에서 할당될 객체의 포인터를 메모리 매니저가 관리하게 하여 추가적인 실수를 줄여야한다.
성능과 최적화 성능 최적화 가독성 확장성 확장성 가독성 성능 최적화 한쪽에 비중을 두게 되면 다른한쪽이 부족해진다. 엔진 혹은 렌더링툴 같은 특정기능이 강화되어야하는프로그램들은 완성후 어느정도의 가독성과 확장성을 포기하고 성능을 향상시켜 프로그램의 퍼포먼스를 높인다. 하지만 학생들을 가르치기위한 간단한 프로그램 예제 같은 경우에는 좀더 이해하기쉽게 가독성에 초점을 맞춰 성능을 포기한다. 게임 개발의 경우에는 어느 한쪽에 초점을 두고 시작하는것이 아니라 개발진행중과 끝난 시점을 나눠 그 상황에 필요한 작업을 수행하게 해야한다. 렉이 심하면 성능우선, 게임내 새로운 요소를 추가구현 할경우 확장성,가독성 우선시. 우리는 게임 개발에 있어 어느 상황에 어떤 방법을 택할것인지 알아야한다.
성능이 모든 것은 아니다.
고려해볼 요인들 함수 타입. 복사. 생성과 소멸. 캐시성능과 메모리.
함수 타입 비가상함수의 오버헤드는 전역함수와 비슷하다. 단지 전달인자로 객체의 포인터를 넘겨준다는 것만 다르다. 함수의 주소가 위치한 메모리로 점프와 코드 캐시에 관련된 비용소모. 함수호출에 따른 성능저하를 생각하지 말자. 전역함수 멤버함수 단일 상속 OR 다중 상속 - 좀더 작은 단위로 솔류션개발 가능. - 코드관리 재사용. - 하나의 함수와 하나의 기능구현 좀더 쉽게 목적 파악 가능. - 크기가 작은 함수는 인라인으로 성능 향상을 할 수 있다. 가상함수 비가상함수의 오버헤드는 전역함수와 비슷하다. 단지 전달인자로 객체의 포인터를 넘겨준다는 것만 다르다. 다중이나 단일상속의 가상함수는 vtable로의 참조에따른 오버헤드가 추가된다. 단일상속의 경우에는 상속의 깊이가 성능에 영향을 주지 못하나(각각의 클래스는 자신의 vtable를 가지므로), 다중일경우 vtable의 크기가 커질수있다. 비 가상함수
복사 함수에 이어서 설명 한다. 전달인자 복사, 대입복사, 리턴시 임시복사 전달 인자 대입 리턴 복사되는 상황 Func( CTest _t ) Ctest t = PreT return new CTest() 문제점 큰 객체의 복사비용 해결 방법 참조(&) 참조, 복사생성자 반환값 최적화 사용. 불필요한 복사를 막자. : const 참조이용, explicit 이용, 임시복사 막기(자동 형변환으로인한 문제), noncopyable특성이용. 복사를 허용해야할 경우에는 특정한 기능(복사기능)을 하는 함수를 준비함. : Clone(), Copy(). 특정 타입의 연산자 오버로딩. : +(이항연산자)를 +=(단항)으로 대체. 참조에의한 this리턴. 함수에 이어서 설명 한다. 전달인자 복사, 대입복사, 리턴시 임시복사 성능저하의 원인이되는 연산자오버로딩은 작업의 대상이 되는 형의 객체를 리턴한다. ROV에대해 보충설명. 함수를 인라인으로 만들고 반환값을 생성자로만듬. return Test(); 컴파일러가 반환객체를 함수에서 반환받는객체로 대체하여 임시객체가 사라진다. 주의할점은 함수의 모든 경로에서 같은 값을 리턴해줘야 최적화 옵션이 작동한다.
생성과 소멸 생성비용이 큰 클래스일경우 초기화 리스트를 이용한다. 잦은 생성과 소멸이 문제가 된다. : 지연로딩으로 해결. 프록시 패턴을 사용해 본다. Client Subject Proxy realSubject RealSubject 실제 객체로의 접근을 제어한다. 그 객체를 사용하는 시점까지 생성과 초기화에 드는 비용을 아낄수 있다.
(캐시)성능과 메모리 캐시메모리는 메모리의 어떤 한바이트를 읽어들이면 프로그램 개발 후반에 특정 루프가 예상보다 느리게 동작하면 캐시 미스에 의한 것으로 생각해 볼 수 있다. 메모리 0x0100 0x0200 0x0300 0x0400 0x0500 0x0600 0x0700 0x0240 위치의 데이터 32byte ... 캐시메모리는 메모리의 어떤 한바이트를 읽어들이면 그에 대응되는 32바이트의 데이터가 한번에 캐시 라인에 보관된다. 즉, 캐시에 보관될 객체의 크기를 32바이트로 만들거나 배수로 만들고, 자주사용되는 클래스의 멤버 변수를 가장 상위로 올린다. 주의할 점은 캐시로 읽어 들일 데이터들의 주소가 32바이트 배수주소로 정렬되있어야한다. 캐시성능의 멀티 스레드의 경우 단점(프로그램의 실행 시간을 결정하기 어렵다)도 역시 존재한다. CPU마다 각각의 캐시를 가져 한 스레드가 바꾼 메모리의 값을 다른 스레드가 곧바로 볼수 있다는 보장을 하기 어렵다.
정리 최고의 개발 도구는 컴퓨터가 아닌 사람이다. 메모리 관리자와 메모리 풀을 이용한다. STL 어댑터를 이용하여 게임프로그램상의 로직과 알고리즘 부분을 분리. 프로그램 개발 도중에는 성능에 집착하지 말자. : 결과가 프로그램전체에 큰 영향을 미칠경우에 성능을 고려해야한 최적화는 유지보수의 한 방법이라고 생각 한다. 발전 모습 : 프로그램의 확장성과 최적의 성능을 유지한 모습으로. 최고의 개발 도구는 컴퓨터가 아닌 사람이다.
Q&A