엔터티 / 컴포넌트 시스템에서 엔터티 동시 처리를위한 읽기 / 계산 / 쓰기 단계를 효율적으로 분리


11

설정

엔터티가 일련의 속성 (동작이없는 순수한 데이터)을 가질 수있는 엔티티 구성 요소 아키텍처를 가지고 있으며 해당 데이터에서 작동하는 엔티티 논리를 실행하는 시스템이 있습니다. 본질적으로 다소 의사 코드에서 :

Entity
{
    id;
    map<id_type, Attribute> attributes;
}

System
{
    update();
    vector<Entity> entities;
}

일정한 속도로 모든 엔티티를 따라 이동하는 시스템은

MovementSystem extends System
{
   update()
   {
      for each entity in entities
        position = entity.attributes["position"];
        position += vec3(1,1,1);
   }
}

본질적으로 가능한 효율적으로 update ()를 병렬화하려고합니다. 이것은 전체 시스템을 병렬로 실행하거나 한 시스템의 각 update ()에 두 개의 구성 요소를 제공하여 다른 스레드가 동일한 시스템의 업데이트를 실행할 수 있지만 해당 시스템에 등록 된 엔티티의 다른 하위 집합에 대해 수행 할 수 있습니다.

문제

표시된 MovementSystem의 경우 병렬화는 간단합니다. 엔터티는 서로 의존하지 않고 공유 데이터를 수정하지 않기 때문에 모든 엔터티를 병렬로 이동할 수 있습니다.

그러나 이러한 시스템은 때때로 엔티티가 서로 동일한 시스템 내에서 서로 상호 작용 (데이터 읽기 / 쓰기)해야하지만 때로는 서로 다른 시스템간에 상호 작용해야합니다.

예를 들어, 물리 시스템에서 때로는 개체가 서로 상호 작용할 수 있습니다. 두 객체가 충돌하여 위치, 속도 및 기타 속성을 읽고 업데이트 한 다음 업데이트 된 속성을 두 엔터티에 다시 씁니다.

엔진의 렌더링 시스템이 엔티티 렌더링을 시작하기 전에 다른 시스템이 실행을 완료하여 모든 관련 속성이 필요한지 확인해야합니다.

우리가 이것을 맹목적으로 병렬화하려고 시도하면 다른 시스템이 동시에 데이터를 읽고 수정할 수있는 고전적인 경쟁 조건으로 이어질 것입니다.

이상적으로는 다른 시스템이 동일한 데이터를 동시에 수정하는 것에 대해 걱정할 필요없이 프로그래머가 원하는 실행 및 병렬화를 적절히 순서화 할 필요없이 모든 시스템이 원하는 엔티티로부터 데이터를 읽을 수있는 솔루션이 존재합니다. 이러한 시스템을 수동으로 (때로는 불가능할 수도 있음).

기본 구현에서는 모든 데이터 읽기 및 쓰기를 중요한 섹션에 넣는 것 (뮤텍스로 보호)을 통해 달성 할 수 있습니다. 그러나 이로 인해 많은 양의 런타임 오버 헤드가 발생하며 성능에 민감한 응용 프로그램에는 적합하지 않을 수 있습니다.

해결책?

내 생각에 가능한 해결책은 데이터 읽기 / 업데이트 및 쓰기가 분리 된 시스템이므로 하나의 비싼 단계에서 시스템은 데이터를 읽고 계산 해야하는 것을 계산하고 결과를 캐시 한 다음 모든 것을 쓸 수 있습니다. 변경된 데이터는 별도의 쓰기 패스로 대상 엔터티로 다시 전달됩니다. 모든 시스템은 데이터가 프레임의 시작 부분에 있던 상태에서 데이터에 대해 작동 한 다음 프레임이 끝나기 전에 모든 시스템의 업데이트가 완료되면 직렬화 된 쓰기 패스가 발생합니다. 시스템은 대상 엔티티를 통해 반복되어 다시 작성됩니다.

이것은 쉬운 병렬화 승리가 결과 캐싱 및 쓰기 패스의 비용 (런타임 성능 및 코드 오버 헤드 측면에서)을 능가 할만큼 충분히 클 수 있다는 아이디어를 기반으로합니다.

질문

최적의 성능을 달성하기 위해 그러한 시스템을 어떻게 구현할 수 있습니까? 이러한 시스템의 구현 세부 사항은 무엇이며이 솔루션을 사용하려는 엔티티 구성 요소 시스템의 전제 조건은 무엇입니까?

답변:


1

----- (수정 된 질문에 근거)

첫 번째 요점 : 릴리스 빌드 런타임을 프로파일 링하고 특정 요구 사항을 발견 한 것에 대해서는 언급하지 않았으므로 최대한 빨리 제안하는 것이 좋습니다. 프로파일이 어떻게 생겼는지, 나쁜 메모리 레이아웃으로 캐시를 스 래싱하고, 100 %로 페그 된 하나의 코어, ECS를 처리하는 데 소요되는 상대 시간, 나머지 엔진 등 ...

엔터티에서 읽고 무언가를 계산하고 나중에 중간 저장 영역 어딘가에 결과를 유지합니까? 나는 당신이 생각 하고이 중간 저장소가 순수한 오버 헤드가 아닌 것으로 예상하는 방식으로 읽기 + 계산 + 저장소를 분리 할 수 ​​있다고 생각하지 않습니다.

또한 연속 처리를 수행하기 때문에 따라야 할 주요 규칙은 CPU 코어 당 하나의 스레드를 갖는 것입니다. 나는 당신이 이것을 잘못된 계층에서보고 있다고 생각합니다 . 개별 엔티티가 아닌 전체 시스템을보십시오.

시스템간에 종속성 그래프를 작성하십시오. 이전 시스템 작업의 결과로 필요한 시스템 트리입니다. 해당 종속성 트리가 있으면 전체 시스템을 엔터티로 전체를 보내 스레드 에서 처리 할 수 있습니다 .

따라서 의존성 트리는 디자인 문제인 덤불과 곰 덫에 걸리지 만 우리가 가지고있는 것을 다루어야한다고 가정 해 봅시다. 여기서 가장 좋은 경우는 각 시스템 내에서 각 엔티티가 해당 시스템 내의 다른 결과에 의존하지 않는 것입니다. 여기서는이 시스템이 소유하는 2 개의 코어와 200 개의 엔티티가있는 예를 위해 2 개의 스레드에서 0-99 및 100-199 스레드로 처리를 쉽게 세분화합니다.

두 경우 모두 각 단계에서 다음 단계가 의존하는 결과를 기다려야합니다. 그러나 대량으로 처리되는 10 개의 큰 데이터 블록의 결과를 기다리는 것이 작은 블록에 대해 수천 번 동기화하는 것보다 훨씬 우수하기 때문에 이것은 괜찮습니다.

의존성 그래프를 구축하는 아이디어는 자동화를 통해 "다른 시스템을 찾아서 병렬로 실행하는 것"의 불가능한 작업을 사소하게 만드는 것이 었습니다. 이러한 그래프가 이전 결과를 지속적으로 대기하여 차단 된 징후를 나타내는 경우 읽기 + 수정 및 지연된 쓰기를 작성하면 차단 만 이동하고 처리의 직렬 특성을 제거하지 않습니다.

그리고 직렬 처리는 각 시퀀스 포인트간에 만 병렬로 전환 할 수 있지만 전체적으로는 불가능합니다. 그러나 이것이 문제의 핵심이기 때문에 이것을 알고 있습니다. 아직 작성되지 않은 데이터에서 캐시 읽기를 수행하더라도 해당 캐시를 사용할 수 있으려면 기다려야합니다.

이러한 종류의 제약으로 병렬 아키텍처를 만드는 것이 쉬웠거나 가능하다면 Bletchley Park 이후 컴퓨터 과학은 문제로 어려움을 겪지 않았을 것입니다.

유일한 실제 솔루션은 이러한 모든 종속성최소화 하여 시퀀스 포인트를 가능한 한 거의 필요로하지 않는 것입니다. 여기에는 각 서브 시스템 내부에서 스레드와 병렬로 진행하는 것이 사소한 순차적 인 처리 단계로 시스템을 세분화 하는 것이 포함될 수 있습니다 .

내가이 문제에 가장 좋은 것은 벽돌 벽에 머리를 때리면 머리를 작은 벽돌 벽으로 쪼개면 빛을 치는 것만 권장하는 것 이상입니다.


유감스럽게도이 답변은 비생산적인 것 같습니다. 당신은 내가 찾고있는 것이 존재하지 않는다고 말하고 있습니다. 논리적으로 틀린 것처럼 보입니다 (적어도 원칙적으로). 그리고 사람들이 이전에 여러 곳에서 그러한 시스템을 암시하는 것을 보았 기 때문에 (아무도 충분히 줄 사람이 없습니다) 그러나이 질문을하는 주된 동기는 무엇입니까? 비록 원래 질문에 충분히 상세하지 않았기 때문에 광범위하게 업데이트 한 이유입니다 (마음이 넘어지면 계속 업데이트 할 것입니다).
TravisG

또한 공격 의도가 없습니다 : P
TravisG

@TravisG Patrick이 지적한 것처럼 다른 시스템에 의존하는 시스템이 종종 있습니다. 프레임 지연을 피하거나 논리 단계의 일부로 다중 업데이트 패스를 피하기 위해 허용되는 솔루션은 업데이트 단계를 직렬화하여 가능한 경우 서브 시스템을 병렬로 실행하고 서브 시스템을 종속성이있는 직렬화하는 동시에 작은 업데이트 패스를 일괄 처리하는 것입니다. parallel_for () 개념을 사용하는 서브 시스템. 서브 시스템 업데이트 패스 요구와 가장 유연한 조합에 이상적입니다.
Naros

0

이 문제에 대한 흥미로운 해결책을 들었습니다. 엔터티 데이터의 사본이 2 개있을 것이라는 아이디어가 있습니다. 하나는 현재 사본이고 다른 하나는 과거 사본입니다. 현재 사본은 쓰기 전용이며 과거 사본은 읽기 전용입니다. 시스템이 동일한 데이터 요소에 쓰고 싶지 않다고 가정하지만 그렇지 않은 경우 해당 시스템은 동일한 스레드에 있어야합니다. 각 스레드는 상호 배타적 인 데이터 섹션의 현재 사본에 대한 쓰기 액세스 권한을 가지며 모든 스레드는 데이터의 모든 이전 사본에 대한 읽기 액세스 권한을 가지므로 이전 사본의 데이터를 사용하지 않고 현재 사본을 업데이트 할 수 있습니다 잠금. 각 프레임 사이에서 현재 사본이 과거 사본이되지만 역할 교환을 처리하려고합니다.

이 방법은 또한 모든 시스템이 시스템이 처리하기 전 / 후에 변경되지 않는 오래된 상태로 작동하기 때문에 경쟁 조건을 제거합니다.


John Carmack의 힙 복사 기술입니까? 나는 그것에 대해 궁금해했지만 여전히 여러 스레드가 동일한 출력 위치에 쓸 수있는 동일한 문제가 있습니다. 모든 것을 "단일 패스"로 유지하면 좋은 해결책 일 것입니다. 그러나 그것이 얼마나 실현 가능한지 잘 모르겠습니다.
TravisG

GUI 반응성을 포함하여 화면에 표시되는 대기 시간이 1 프레임 시간만큼 증가합니다. 액션 / 타이밍 게임 또는 RTS와 같은 GUI 조작이 중요 할 수 있습니다. 그러나 나는 그것을 창의적인 아이디어로 좋아한다.
Patrick Hughes

나는 친구로부터 이것에 대해 들었고 그것이 Carmack 트릭이라는 것을 몰랐습니다. 렌더링이 수행되는 방식에 따라 구성 요소 렌더링이 한 프레임 뒤에있을 수 있습니다. 업데이트 단계에 이것을 사용하고 모든 것이 최신 상태이면 현재 사본에서 렌더링 할 수 있습니다.
존 맥도날드

0

데이터 병렬 처리를 처리하는 3 가지 소프트웨어 설계를 알고 있습니다.

  1. 데이터를 순차적으로 처리 : 여러 스레드를 사용하여 데이터를 처리하려고하므로 더 이상하게 들릴 수 있습니다. 그러나 대부분의 시나리오에서는 작업을 완료하기 위해 여러 스레드가 필요하지만 다른 스레드는 대기하거나 오래 실행되는 작업을 수행합니다. 가장 일반적인 사용법은 단일 스레드에서 사용자 인터페이스를 업데이트하는 UI 스레드이며 다른 스레드는 백그라운드에서 실행될 수 있지만 UI 요소에 직접 액세스 할 수는 없습니다. 백그라운드 스레드의 결과를 전달하기 위해 다음 번 적절한 기회에 단일 스레드에서 처리 할 작업 큐 가 사용됩니다.
  2. 데이터 액세스 동기화 : 동일한 데이터에 액세스하는 여러 스레드를 처리하는 가장 일반적인 방법입니다. 대부분의 프로그래밍 언어에는 여러 스레드가 동시에 데이터를 읽고 쓰는 섹션 을 잠그기 위해 클래스와 도구가 내장되어 있습니다. 그러나 작업을 차단하지 않도록주의해야합니다. 반면에이 방법은 실시간 응용 프로그램에서 많은 오버 헤드가 발생합니다.
  3. 동시 수정 이 발생할 때만 처리하십시오 . 충돌이 거의 발생하지 않는 경우이 낙관적 접근 방식을 수행 할 수 있습니다. 다중 액세스가 전혀 없으면 데이터를 읽고 수정하지만 데이터가 동시에 업데이트되는시기를 감지하는 메커니즘이 있습니다. 이 경우 단일 계산은 성공할 때까지 다시 실행됩니다.

다음은 엔터티 시스템에서 사용될 수있는 각 접근 방식에 대한 몇 가지 예입니다.

  1. 생각하자 CollisionSystemPositionRigidBody구성 요소와 업데이트해야합니다 Velocity. Velocity직접 조작하는 대신의 작업 대기열에 CollisionSystem를 넣습니다 . 이 이벤트는에 대한 다른 업데이트와 함께 순차적으로 처리됩니다 .CollisionEventEventSystemVelocity
  2. EntitySystem이 읽기 및 쓰기하는 데 필요한 구성 요소 세트를 정의합니다. 각각에 대해 Entity그것은 aquire 것이다 읽기 잠금 읽기가하고자하는 각 구성 요소 및 쓰기 잠금 이 업데이트하고자하는 각 구성 요소를. 이와 같이 EntitySystem업데이트 작업이 동기화되는 동안 모든 사람 이 동시에 구성 요소를 읽을 수 있습니다.
  3. 의 예를 들어 MovementSystem, Position구성 요소는 변경할 수 없으며 개정 번호 가 포함되어 있습니다 . 는 MovementSystemsavely 읽기 PositionVelocity구성 요소를 새로운를 계산 Position읽기 증가, 개정 번호와 갱신 시도 Position구성 요소를. 동시 수정의 경우 프레임 워크는 업데이트시이를 표시하고에 의해 업데이트 Entity되어야하는 엔티티 목록에 다시 표시됩니다 MovementSystem.

시스템, 엔터티 및 업데이트 간격에 따라 각 방법이 좋거나 나쁠 수 있습니다. 엔터티 시스템 프레임 워크를 통해 사용자는 이러한 옵션 중에서 선택하여 성능을 조정할 수 있습니다.

토론에 아이디어를 추가 할 수 있기를 바랍니다. 뉴스가 있으면 알려주세요.

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