동적 메모리 할당 및 메모리 관리


17

보통 게임에서는 장면에 수백 또는 수천 개의 물체가 있습니다. 기본 샷 (bullet)을 포함한 모든 객체에 기본 new () 를 통해 동적으로 메모리를 할당하는 것이 완벽 합니까?

동적 할당을 위해 메모리 풀을 만들어야합니까 , 아니면 이것을 귀찮게 할 필요가 없습니까? 대상 플랫폼이 모바일 장치 인 경우 어떻게합니까?

모바일 게임에서 메모리 관리자 가 필요 합니까? 감사합니다.

사용되는 언어 : C ++; 현재 Windows에서 개발되었지만 나중에 이식 될 계획입니다.


어느 언어?
Kylotan

@Kylotan : 사용되는 언어 : C ++은 현재 Windows에서 개발되었지만 나중에 이식 될 계획입니다.
Bunkai.Satori

답변:


23

평균 게임에서는 장면에 수백 또는 수천 개의 개체가 있습니다. 기본 new ()를 통해 동적으로 총 발사 (총알)를 포함하여 모든 객체에 메모리를 할당하는 것이 정확합니까?

그것은 "정확한"의 의미에 따라 다릅니다. 용어를 문자 그대로 받아들이고 (묵시적 디자인의 정확성 개념을 무시한 경우) 그렇습니다. 당신의 프로그램은 잘 컴파일되고 실행될 것입니다.

차선의 성능을 발휘할 수는 있지만 여전히 배송이 가능하고 재미있는 게임이되기에 충분한 성능을 발휘할 수 있습니다.

동적 할당을 위해 메모리 풀을 만들어야합니까, 아니면 이것을 귀찮게 할 필요가 없습니까? 대상 플랫폼이 모바일 장치 인 경우 어떻게합니까?

프로필을 참조하십시오. 예를 들어 C ++에서 힙에 대한 동적 할당은 일반적으로 "느린"작업입니다 (적절한 크기의 블록을 찾기 위해 힙을 걷는 과정이 포함됨). C #에서는 일반적으로 증분 이상의 작업을 수행하기 때문에 매우 빠른 작업입니다. 상이한 언어 구현은 메모리 할당, 해제시 단편화 등과 관련하여 상이한 성능 특성을 갖는다.

메모리 풀링 시스템을 구현하면 성능이 향상 될 수 있습니다. 모바일 시스템은 일반적으로 데스크탑 시스템에 비해 성능이 저하되므로 데스크탑에서보다 특정 모바일 플랫폼에서 더 많은 이점을 얻을 수 있습니다. 그러나 다시 한 번 프로파일 링하고 확인해야합니다. 현재 게임 속도는 느리지 만 메모리 할당 / 릴리스가 프로파일 러에 핫스팟으로 표시되지 않으면 메모리 할당 및 액세스를 최적화하기위한 인프라를 구현하여 아마도 당신은 당신의 돈을 많이 쾅 얻을.

모바일 게임에서 메모리 관리자가 필요합니까? 감사합니다.

다시, 프로파일하고 참조하십시오. 지금 게임이 잘 돌아가고 있습니까? 그러면 걱정할 필요가 없습니다.

모든주의에 대해 동적 할당을 사용하는 것이 엄격하게 필요한 것은 아니므로 잠재적 인 성능 향상과 추적 및 결국 해제 해야하는 메모리 할당으로 인해 피하는 것이 유리할 수 있습니다 즉, 코드를 추적하고 릴리스해야 코드가 복잡해질 수 있습니다.

특히, 원래 예제에서 많은 글 머리 기호가 포함 된 많은 게임이 많고 글 머리 기호가 빠르게 이동하여 수명이 빨리 끝나기 때문에 자주 생성되고 파괴되는 경향이있는 "글 머리 기호"를 인용했습니다. 맹렬히!). 따라서 파티클 시스템의 파티클과 같은 파티클과 파티클에 대해 풀 할당자를 구현하면 일반적으로 효율성이 향상 될 수 있으며 풀 할당 사용을 가장 먼저 살펴볼 수 있습니다.

메모리 풀 구현이 "메모리 관리자"와 구별되는 것을 고려하고 있는지 확실하지 않습니다. 메모리 풀은 비교적 잘 정의 된 개념이므로 구현하면 이점이 될 수 있다고 확신 할 수 있습니다. . "메모리 관리자"는 책임면에서 좀 더 모호하므로 "메모리 관리자"가 필요하다고 생각하는 것에 따라 필요 여부가 결정되어야합니다.

예를 들어 메모리 관리자가 new / delete / free / malloc / what에 대한 호출을 가로 채고 할당하는 메모리 양, 누출 된 부분 등에 대한 진단을 제공하는 것으로 생각되면 유용 할 수 있습니다. 게임을 개발하는 동안 누출을 디버그하고 최적의 메모리 풀 크기 등을 조정하는 데 도움이되는 도구입니다.


동의했다. 나중에 변경할 수있는 방법으로 코드를 작성하십시오. 의심스러운 경우 벤치 마크 또는 프로파일.
axel22

@ Josh : 탁월한 답변을 위해 +1. 아마도 필요한 것은 동적 할당, 정적 할당 및 메모리 풀의 조합입니다. 그러나 게임의 성능은 저 세 가지를 적절히 혼합하여 안내합니다. 이것은 내 질문에 대한 합격 답변의 명확한 후보입니다 . 그러나 나는 다른 사람들이 무엇을 할 것인지 알기 위해 잠시 동안 질문을 열어두고 싶습니다.
Bunkai.Satori

+1. 뛰어난 정교함. 거의 모든 성능 질문에 대한 답변은 항상 "프로필 및보기"입니다. 요즘 하드웨어는 너무 복잡하여 첫 번째 원칙의 성능에 대해 추론하기가 어렵습니다. 데이터가 필요합니다.
munificent

@Munificent : 의견 주셔서 감사합니다. 그래서 목표는 게임 작업과 스터브를 만드는 것입니다. 개발 도중 성능에 대해 너무 걱정할 필요가 없습니다. 게임 완료 후 모든 것이 고칠 수 있습니다.
Bunkai.Satori

나는 이것이 C #의 할당 시간을 부당하게 표현한다고 생각합니다. 예를 들어 모든 C # 할당에는 동기화 블록, 객체 할당 등이 포함됩니다. 또한 C ++의 힙에는 할당 및 해제시에만 수정이 필요하지만 C #에는 컬렉션이 필요합니다 .
DeadMG

7

Josh의 훌륭한 답변에 추가 할 것이 많지 않지만 이에 대해 언급 할 것입니다.

동적 할당을 위해 메모리 풀을 만들어야합니까, 아니면 이것을 귀찮게 할 필요가 없습니까?

메모리 풀과 new각 할당을 호출하는 중간 지점이 있습니다 . 예를 들어, 배열에 설정된 수의 객체를 할당 한 다음 나중에 '파기'하도록 플래그를 설정할 수 있습니다. 더 많이 할당해야 할 경우 파괴 된 플래그 세트로 덮어 쓸 수 있습니다. 이런 종류의 것은 새로운 / 삭제보다 사용하기가 약간 복잡하지만 (그 목적을 위해 2 개의 새로운 기능을 가지므로) 작성하기 쉽고 큰 이익을 줄 수 있습니다.


좋은 추가 +1. 예, 맞습니다. 총알, 입자, 효과와 같은 간단한 게임 요소를 관리하는 좋은 방법입니다. 특히 이러한 경우에는 메모리를 동적으로 할당 할 필요가 없습니다.
Bunkai.Satori

3

기본 샷 (bullet)을 포함한 모든 객체에 기본 new ()를 통해 동적으로 메모리를 할당하는 것이 완벽합니까?

물론 아닙니다. 모든 객체에 대해 올바른 메모리 할당이 없습니다 . 연산자 new ()는 동적 할당을 위한 것입니다. 즉, 객체의 수명이 동적이거나 객체의 유형이 동적이기 때문에 할당이 동적이어야하는 경우에만 적합합니다. 객체의 유형과 수명이 정적으로 알려진 경우 정적으로 할당해야합니다.

물론 할당 패턴에 대한 정보가 많을수록 객체 풀과 같은 전문 할당자를 통해 이러한 할당을 더 빠르게 수행 할 수 있습니다. 그러나 이는 최적화이며 필요한 것으로 알려진 경우에만 최적화해야합니다.


좋은 답변을 얻으려면 +1하십시오. 일반화하기 위해 올바른 접근 방법은 다음과 같습니다. 개발 초기에 정적으로 할당 할 수있는 객체를 계획하는 것입니다. 개발 중에 동적으로 할당해야하는 객체 만 동적으로 할당합니다. 결국 가능한 메모리 할당 성능 문제를 프로파일 링하고 조정합니다.
Bunkai.Satori

0

Kylotan의 제안을 반향하지만 가능한 한 낮은 할당 자 수준이 아니라 가능한 경우 데이터 구조 수준 에서이 문제를 해결하는 것이 좋습니다.

다음 Foos은 요소가 연결된 구멍이있는 배열을 사용하여 반복적으로 할당 및 해제를 피할 수있는 간단한 예입니다 ( "할당 자"레벨이 아닌 "컨테이너"레벨에서 해결).

struct FooNode
{
    explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}

    // Stores a 'Foo'.
    Foo element;

    // Points to the next foo available; either the
    // next used foo or the next deleted foo. Can
    // use SoA and hoist this out if Foo doesn't 
    // have 32-bit alignment.
    int next;
};

struct Foos
{
    // Stores all the Foo nodes.
    vector<FooNode> nodes;

    // Points to the first used node.
    int first_node;

    // Points to the first free node.
    int free_node;

    Foos(): first_node(-1), free_node(-1)
    {
    }

    const FooNode& operator[](int n) const
    {
         return data[n];
    }

    void insert(const Foo& element)
    {
         int index = free_node;
         if (index != -1)
         {
              // If there's a free node available,
              // pop it from the free list, overwrite it,
              // and push it to the used list.
              free_node = data[index].next;
              data[index].next = first_node;
              data[index].element = element;
              first_node = index;
         }
         else
         {
              // If there's no free node available, add a 
              // new node and push it to the used list.
              FooNode new_node(element);
              new_node.next = first_node;
              first_node = data.size() - 1;
              data.push_back(new_node);
         }
    }

    void erase(int n)
    {
         // If the node being removed is the first used
         // node, pop it from the used list.
         if (first_node == n)
              first_node = data[n].next;

         // Push the node to the free list.
         data[n].next = free_node;
         free_node = n;
    }
};

이 결과에 영향을주는 것 : 사용 가능한 목록이있는 단일 연결 색인 목록. 인덱스 링크를 사용하면 제거 된 요소를 건너 뛰고, 일정한 시간에 요소를 제거하고, 일정한 시간 삽입으로 사용 가능한 요소를 회수 / 재사용 / 덮어 쓸 수 있습니다. 구조를 반복하려면 다음과 같이하십시오.

for (int index = foos.first_node; index != -1; index = foos[index].next)
    // do something with foos[index]

여기에 이미지 설명을 입력하십시오

또한 템플릿을 사용하여 위의 "링크 된 홀 배열"데이터 구조를 일반화 할 수 있으며, 사본 할당에 대한 요구 사항을 피하기 위해 배치 및 신규 및 수동 dtor 호출을 배치하고 요소를 제거 할 때 소멸자를 호출하고 순방향 반복자를 제공하는 등의 작업을 수행 할 수 있습니다. 개념을보다 명확하게 설명하기 위해 예제를 C와 같이 유지하기로 선택했으며 매우 게으 르기 때문입니다.

즉,이 구조는 중간에서 많은 것을 제거하고 삽입 한 후에 공간 위치가 저하되는 경향이 있습니다. 이 시점에서 next링크를 통해 벡터를 따라 앞뒤로 이동할 수 있으며, 동일한 순차 순회 내에 캐시 라인에서 이전에 제거 된 데이터를 다시로드 할 수 있습니다 (이는 데이터 구조 또는 할당 자에서 불가피합니다. 상수 비트 삽입 또는 병렬 비트 세트 또는 removed플래그 와 같은 것을 사용하지 않고 중간부터 공백 ). 캐시 친 화성을 복원하기 위해 다음과 같이 copy ctor 및 swap 메소드를 구현할 수 있습니다.

Foos(const Foos& other)
{
    for (int index = other.first_node; index != -1; index = other[index].next)
        insert(foos[index].element);
}

void Foos::swap(Foos& other)
{
     nodes.swap(other.nodes):
     std::swap(first_node, other.first_node);
     std::swap(free_node, other.free_node);
}

// ... then just copy and swap:
Foos(foos).swap(foos);

이제 새 버전은 캐시 친화적이므로 다시 통과합니다. 또 다른 방법은 별도의 인덱스 목록을 구조에 저장하고 주기적으로 정렬하는 것입니다. 또 다른 방법은 사용 된 인덱스를 나타 내기 위해 비트 세트를 사용하는 것입니다. 이를 통해 항상 비트 세트를 순차적으로 순회해야합니다 (이를 효율적으로 수행하려면 한 번에 64 비트를 확인하십시오 (예 : FFS / FFZ 사용)). 비트 세트는 가장 효율적이고 방해가되지 않으며 요소 당 병렬 비트 만 필요하며 32 비트 next인덱스 를 요구하는 대신 사용되는 비트와 제거 된 비트 를 나타내지 만 잘 작성하는 데 가장 많은 시간이 소요됩니다. 한 번에 하나의 비트를 확인하는 경우 순회 속도가 빠릅니다. 점유 된 인덱스의 범위를 신속하게 결정하려면 한 번에 32 개 이상의 비트 중에서 세트 또는 설정되지 않은 비트를 찾으려면 FFS / FFZ가 필요합니다.

이 링크 된 솔루션은 일반적으로 구현하기 가장 쉽고 방해가되지 않습니다 ( 플래그 Foo를 저장하기 위해 수정 할 필요가 없음 removed). 요소 당 오버 헤드.

동적 할당을 위해 메모리 풀을 만들어야합니까, 아니면 이것을 귀찮게 할 필요가 없습니까? 대상 플랫폼이 모바일 장치 인 경우 어떻게합니까?

필요 는 강력한 단어이며 레이트 레이싱, 이미지 처리, 입자 시뮬레이션 및 메쉬 처리와 같은 성능이 매우 중요한 영역에서 일하는 편견이 있지만 총알과 같은 매우 가벼운 처리에 사용되는 조그마한 객체를 할당하고 해제하는 것은 비교적 비용이 많이 듭니다. 및 범용의 가변 크기 메모리 할당기에 대해 개별적으로 파티클을 포함한다. 원하는 데이터를 저장하기 위해 하루나 이틀 안에 위의 데이터 구조를 일반화 할 수 있어야한다고 생각할 때마다 이러한 힙 할당 / 할당 비용을 하나의 작은 물건에 대해 지불하는 것을 제거하는 것이 가치있는 교환이라고 생각합니다. 할당 / 할당 비용을 줄이는 것 외에도 결과를 통과하는 참조의 지역성이 향상됩니다 (캐시 누락 및 페이지 오류 감소).

Josh가 GC에 대해 언급 한 바에 따르면, C #의 GC 구현을 Java와 거의 비슷하게 연구하지는 않았지만 GC 할당자는 종종 초기 할당이 있습니다.그것은 중간에서 메모리를 확보 할 수없는 순차적 할당자를 사용하기 때문에 매우 빠릅니다 (거의 스택과 마찬가지로 중간에서 물건을 삭제할 수는 없습니다). 그런 다음 실제로 메모리를 복사하고 이전에 할당 된 메모리를 전체적으로 제거하여 개별 스레드에서 개별 객체를 제거 할 수있게하는 비용이 많이 듭니다 (데이터를 링크 된 구조와 같은 것으로 복사하는 동시에 전체 스택을 한 번에 파괴 함). 그러나 별도의 스레드에서 수행되기 때문에 응용 프로그램의 스레드가 너무 많이 정지되지는 않습니다. 그러나 이는 초기 GC주기 이후 추가적인 수준의 간접적 인 비용과 LOR의 일반적인 손실에 대한 막대한 숨겨진 비용을 부담합니다. 그러나 할당 속도를 높이는 또 다른 전략입니다. 호출 스레드에서 더 저렴하게 만든 다음 다른 스레드에서 값 비싼 작업을 수행하십시오. 이를 위해서는 처음 할당 한 시간과 첫 번째주기 이후에 메모리에서 셔플이 발생하기 때문에 하나를 대신하여 두 가지 수준의 간접 참조가 필요합니다.

비슷한 맥락에서 C ++에 적용하기가 쉬운 또 다른 전략은 주 스레드에서 객체를 해제하지 않아도됩니다. 중간에서 물건을 제거 할 수없는 데이터 구조 끝에 추가, 추가 및 추가를 계속하십시오. 그러나 제거해야 할 사항을 표시하십시오. 그러면 별도의 스레드가 제거 된 요소없이 새 데이터 구조를 작성하는 고가의 작업을 처리 한 다음 새 요소를 기존의 요소와 원자 적으로 교환 할 수 있습니다. 요소 제거 요청이 즉시 충족 될 필요가 없다고 가정 할 수있는 경우 별도의 스레드. 스레드에 관한 한 여유가 줄어들뿐만 아니라 할당이 더 저렴합니다. 중간에서 제거 사례를 처리 할 필요가없는 훨씬 단순하고 멍청한 데이터 구조를 사용할 수 있기 때문입니다. 그것은 단지 필요한 컨테이너와 같습니다.push_back삽입을위한 clear기능 , 모든 요소를 ​​제거하고, swap제거 된 요소를 제외한 새로운 소형 용기로 내용물을 교환 하는 기능 ; 그것이 돌연변이가 진행되는 한 그것입니다.

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