캐시 친화적 인 구성 요소 저장소를 사용하여 구성 요소 간 통신을 안전하게 지원하려면 어떻게해야합니까?


9

구성 요소 기반 게임 개체를 사용하는 게임을 만들고 있는데 각 구성 요소가 해당 게임 개체와 통신하는 방법을 구현하는 데 어려움을 겪고 있습니다. 한 번에 모든 것을 설명하는 대신 관련 샘플 코드의 각 부분을 설명하겠습니다.

class GameObjectManager {
    public:
        //Updates all the game objects
        void update(Time dt);

        //Sends a message to all game objects
        void sendMessage(Message m);

    private:
        //Vector of all the game objects
        std::vector<GameObject> gameObjects;

        //vectors of the different types of components
        std::vector<InputComponent> input;
        std::vector<PhysicsComponent> ai;
        ...
        std::vector<RenderComponent> render;
}

GameObjectManager모든 게임 오브젝트 및 그 구성 요소를 보유하고 있습니다. 또한 게임 오브젝트 업데이트도 담당합니다. 구성 요소 벡터를 특정 순서로 업데이트하여이를 수행합니다. 배열 대신 벡터를 사용하므로 한 번에 존재할 수있는 게임 개체 수에 제한이 없습니다.

class GameObject {
    public:
        //Sends a message to the components in this game object
        void sendMessage(Message m);

    private:
        //id to keep track of components in the manager
        const int id;

        //Pointers to components in the game object manager
        std::vector<Component*> components;
}

GameObject클래스는 구성 요소가 무엇인지 알고 메시지를 보낼 수 있습니다.

class Component {
    public:
        //Receives messages and acts accordingly
        virtual void handleMessage(Message m) = 0;

        virtual void update(Time dt) = 0;

    protected:
        //Calls GameObject's sendMessage
        void sendMessageToObject(Message m);

        //Calls GameObjectManager's sendMessage
        void sendMessageToWorld(Message m);
}

Component클래스는 순수한 가상이므로 다양한 유형의 구성 요소에 대한 클래스가 메시지 처리 및 업데이트 방법을 구현할 수 있습니다. 메시지를 보낼 수도 있습니다.

이제 컴포넌트가 및 에서 sendMessage함수를 호출 할 수있는 방법에 대한 문제가 발생합니다 . 나는 두 가지 가능한 해결책을 생각해 냈습니다.GameObjectGameObjectManager

  1. Component대한 포인터를 제공 하십시오 GameObject.

그러나 게임 오브젝트가 벡터에 있기 때문에 포인터가 빠르게 무효화 될 수 있습니다 GameObject. 게임 객체를 배열에 넣을 수는 있지만 크기에 대한 임의의 숫자를 전달해야합니다.

  1. Component대한 포인터를 제공 하십시오 GameObjectManager.

그러나 구성 요소가 관리자의 업데이트 기능을 호출 할 수 있기를 원하지 않습니다. 나는이 프로젝트에서 일하는 유일한 사람이지만 잠재적으로 위험한 코드를 작성하는 습관을 갖고 싶지 않습니다.

코드를 안전하고 캐시 친화적으로 유지하면서이 문제를 어떻게 해결할 수 있습니까?

답변:


6

당신의 커뮤니케이션 모델은 괜찮아 보이고 옵션 포인터는 당신이 그 포인터를 안전하게 저장할 수 있다면 잘 작동 할 것입니다. 구성 요소 스토리지에 대해 다른 데이터 구조를 선택하여이 문제점을 해결할 수 있습니다.

A std::vector<T>는 합리적인 첫 번째 선택이었습니다. 그러나 컨테이너의 반복자 무효화 동작은 문제입니다. 당신이 원하는 것은 반복하는 빠르고 캐시 일관성입니다, 데이터 구조 삽입하거나 항목을 제거 할 때 또한 반복자 안정성을 유지한다.

이러한 데이터 구조를 구축 할 수 있습니다. 링크 된 페이지 목록으로 구성됩니다 . 각 페이지는 용량이 고정되어 있으며 모든 항목을 하나의 배열로 보유합니다. 개수는 해당 배열에서 활성화 된 항목 수를 나타내는 데 사용됩니다. 페이지는 또한 무료 목록 (삭제 항목의 허용 재사용) 당신은 건너 뛸 수 있도록 스킵리스트 (이 이상 반복하는 동안 삭제 항목을.

다시 말해서 개념적으로 다음과 같습니다.

struct Page {
   int count;
   int capacity;           // Optional if every page is a fixed size.
   T * m_storage;
   bool * m_skip;          // Skip list; can be bit-compressed.
   std::stack<int> m_free; // Can be replaced with a specialized stack.

   Page * next;
   Page * prior;           // Optional, allows reverse iteration
};

상상도 할 수없이이 데이터 구조를 이라고 부릅니다 (반복하는 페이지 모음이기 때문에). 구조에는 다른 이름이 있습니다.

매튜 벤틀리는 이것을 "식민지"라고 부릅니다. Matthew의 구현은 이러한 종류의 구조에서보다 일반적인 부울 기반의 건너 뛰기 목록보다 우수한 점프 계산 건너 뛰기 필드 (MediaFire 링크의 사과이지만 Bentley 자신이 문서를 호스팅하는 방식)를 사용합니다. Bentley의 라이브러리는 헤더 전용이며 모든 C ++ 프로젝트에 쉽게 넣을 수 있으므로 단순히 사용하는 것이 아니라 자신의 롤링을 사용하는 것이 좋습니다. 내가 여기에 영광스럽게 여기는 많은 미묘함과 최적화가 있습니다.

이 데이터 구조는 항목을 추가 한 후에는 항목을 이동하지 않으므로 해당 항목에 대한 포인터 및 반복자는 해당 항목 자체가 삭제되거나 컨테이너 자체가 지워질 때까지 유효합니다. 연속적으로 할당 된 항목의 덩어리를 저장하기 때문에 반복이 빠르며 대부분 캐시 일관성이 있습니다. 삽입과 제거가 합리적입니다.

완벽하지 않습니다. 컨테이너의 효과적으로 임의의 지점에서 크게 삭제 한 다음 후속 삽입이 항목을 백업하기 전에 해당 컨테이너를 반복하는 사용 패턴으로 캐시 일관성을 망칠 수 있습니다. 해당 시나리오에 자주있는 경우 한 번에 잠재적으로 큰 메모리 영역을 건너 뜁니다. 그러나 실제로이 컨테이너는 귀하의 시나리오에 적합한 선택이라고 생각합니다.

다른 답변을 위해 남겨 두려는 다른 접근 방식에는 핸들 기반 접근 방식 또는 슬롯 맵 종류의 구조 (정수 "키"와 정수 "값"의 연관 배열이있는 값)가 포함될 수 있습니다. 백킹 배열에서 추가 인덱스를 사용하여 "인덱스"로 액세스하여 벡터를 반복 할 수 있습니다).


안녕! 마지막 단락에서 언급 한 "콜로니"의 대안에 대해 더 배울 수있는 자료가 있습니까? 그들은 어디서나 구현됩니까? 나는 한동안이 주제를 연구 해 왔고 정말 관심이 있습니다.
Rinat Veliakhmedov

5

'캐시 친화적'인 것은 큰 게임에 대한 선입견 입니다. 이것은 나에게 조기 최적화 된 것 같습니다.


'캐시 친화적'이 아닌이 문제를 해결하는 한 가지 방법은 스택 대신 힙에 오브젝트를 작성하는 것입니다. 오브젝트에 대해 new(스마트) 포인터를 사용하십시오. 이렇게하면 객체를 참조 할 수 있으며 해당 참조는 무효화되지 않습니다.

보다 캐시 친화적 인 솔루션을 위해 객체 할당 해제를 직접 관리하고 이러한 객체에 대한 핸들을 사용할 수 있습니다.

기본적으로 프로그램을 초기화 할 때 객체는 힙에 메모리 덩어리를 예약하고 (MemMan이라고 함) 구성 요소를 만들 때 MemMan에 X 크기의 구성 요소가 필요하다고 알려줍니다. 이를 위해 예약하고, 핸들을 만들고, 할당 된 위치가 해당 핸들의 오브젝트 인 내부에 보관하십시오. 핸들을 반환하고 객체에 대해 유지할 유일한 것은 메모리의 위치를 ​​가리키는 포인터가 아닙니다 .

구성 요소가 필요하면 MemMan에게이 개체에 액세스하도록 요청하면 기꺼이 수행됩니다. 그러나 그것에 대한 참조를 유지하지 마십시오.

MemMan의 작업 중 하나는 메모리에서 객체를 서로 가깝게 유지하는 것입니다. 매 게임 프레임마다, MemMan에게 메모리에 오브젝트를 재 배열하도록 지시 할 수 있습니다 (또는 오브젝트를 생성 / 삭제할 때 자동으로 수행 할 수 있음). 핸들-메모리 위치 맵을 업데이트합니다. 핸들은 항상 유효하지만 메모리 공간에 대한 참조 ( 포인터 또는 참조 )를 유지하면 절망과 황폐 만 발견됩니다.

교과서에 따르면 이러한 메모리 관리 방법에는 두 가지 장점이 있습니다.

  1. 객체가 메모리에서 서로 가까이 있기 때문에 캐시 누락이 적고
  2. 그것은 당신이 가지고 있다고하는 OS로 만들어 줄게 / 할당 통화 드 메모리의 수를 줄일 약간의 시간을.

MemMan을 사용하는 방법과 메모리를 내부적으로 구성하는 방법 은 실제로 구성 요소를 사용 하는 방법에 달려 있습니다. 유형에 따라 구성 요소를 반복하는 경우 구성 요소를 유형별로 유지하고 게임 개체에 따라 구성 요소를 반복하는 경우 구성 요소가 서로 가까운 지 확인하는 방법을 찾아야합니다. 그것에 기초한 또 다른 것 ...

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.