데이터 지향 디자인-1-2 개 이상의 구조 "멤버"에 비현실적입니까?


23

데이터 지향 디자인의 일반적인 예는 Ball 구조입니다.

struct Ball
{
  float Radius;
  float XYZ[3];
};

그런 다음 std::vector<Ball>벡터 를 반복하는 알고리즘을 만듭니다 .

그런 다음 동일한 내용을 제공하지만 데이터 지향 디자인으로 구현됩니다.

struct Balls
{
  std::vector<float> Radiuses;
  std::vector<XYZ[3]> XYZs;
};

모든 반지름을 먼저 반복 한 다음 모든 위치 등을 반복하면 좋을 것입니다. 그러나 벡터에서 공을 어떻게 이동합니까? 원본 버전에서을 사용하는 경우 std::vector<Ball> BallsAllany BallsAll[x]로 이동할 수 있습니다 BallsAll[y].

그러나 데이터 지향 버전의 경우 모든 속성에 대해 동일한 작업을 수행해야합니다 (볼-반경 및 위치의 경우 2 회). 그러나 더 많은 속성이 있으면 더 나빠집니다. 각 "공"에 대해 색인을 유지해야하며, 이동하려고 할 때 모든 속성 벡터에서 이동을 수행해야합니다.

Data Oriented Design의 성능 이점을 없애지 않습니까?

답변:


23

또 다른 대답 은 행 지향 스토리지를 멋지게 캡슐화하고 더 나은 시야를 제공하는 방법에 대한 훌륭한 개요를 제공했습니다. 그러나 성능에 대해서도 질문하기 때문에 SoA 레이아웃은 은색 총알이 아닙니다 . 그것은 꽤 좋은 기본값 (캐시 사용, 대부분의 언어에서 쉽게 구현하기에는별로 중요하지 않음)이지만 데이터 지향 디자인 (정확히 의미하는 것은 아님)조차 전부는 아닙니다. 읽은 일부 소개 작성자가 해당 요점을 놓치고 SoA 레이아웃 만 제시 할 수 있습니다. 왜냐하면 그것이 DOD의 전체 요점이라고 생각하기 때문입니다. 그들은 틀렸을 것입니다. 고맙게도 모든 사람들이 그 함정에 빠지는 것은 아닙니다 .

이미 알고 계시 겠지만, 모든 기본 데이터가 자체 배열로 끌어 당겨지는 것은 아닙니다. SoA 레이아웃은 개별 배열로 분할 된 구성 요소가 일반적으로 개별적으로 액세스 될 때 유리합니다. 그러나 모든 작은 조각이 개별적으로 액세스되는 것은 아닙니다. 예를 들어 위치 벡터는 거의 항상 도매로 읽고 업데이트되므로 자연스럽게 분할하지 않습니다. 사실, 당신의 모범도 그렇게하지 않았습니다! 마찬가지로 일반적으로 볼의 모든 속성에 함께 액세스 하는 경우 대부분의 시간을 볼 모음에서 볼을 교체하는 데 소비하기 때문에 볼을 분리 할 필요가 없습니다.

그러나 국방부의 또 다른 측면이 있습니다. 메모리 레이아웃을 90 ° 돌리고 결과 컴파일 오류를 수정하기 위해 최소한의 노력만으로 캐시와 조직의 이점을 모두 얻지는 못합니다. 이 배너에는 다른 일반적인 트릭이 있습니다. 예를 들어 "존재 기반 처리": 볼을 자주 비활성화하고 다시 다시 활성화하는 경우 볼 객체에 플래그를 추가하지 않고 플래그가 false로 설정된 업데이트 루프에서 볼을 무시하도록합니다. 볼을 "활성"컬렉션에서 "비활성"컬렉션으로 옮기고 업데이트 루프가 "활성"컬렉션 만 검사하도록합니다.

더 중요하고 관련이있는 예 : 볼 배열을 뒤섞는 데 너무 많은 시간을 보낸다면 뭔가 잘못되었을 수 있습니다. 주문이 왜 중요한가요? 중요하지 않게 할 수 있습니까? 그렇다면 몇 가지 이점이 있습니다.

  • 컬렉션을 섞을 필요는 없습니다 (가장 빠른 코드는 코드가 아님).
  • 보다 쉽고 효율적으로 추가 및 삭제할 수 있습니다 (끝으로 교체, 마지막에 놓기).
  • 나머지 코드는 집중적 인 레이아웃 변경과 같은 추가 최적화에 적합 할 수 있습니다.

따라서 모든 것을 맹목적으로 던지는 대신 데이터와 처리 방법에 대해 생각 하십시오. 하나의 루프에서 위치와 속도를 처리 한 다음 메시를 통과 한 다음 적중 점을 업데이트하면 메모리 레이아웃을이 세 부분으로 분할하십시오. 분리 된 위치의 x, y, z 구성 요소에 액세스하는 경우 위치 벡터를 SoA로 바꿀 수 있습니다. 실제로 유용한 것을 수행하는 것보다 더 많은 데이터를 섞는 것을 발견하면 섞는 것을 중단하십시오.


18

데이터 지향적 사고 방식

데이터 지향 디자인이 모든 곳에 SoA를 적용한다는 의미는 아닙니다. 이는 단순히 데이터 표현에 중점을 둔 아키텍처, 특히 효율적인 메모리 레이아웃 및 메모리 액세스에 중점을 둔 아키텍처를 설계하는 것을 의미합니다.

적절한 경우 SoA 담당자가 발생할 수 있습니다.

struct BallSoa
{
   vector<float> x;        // size n
   vector<float> y;        // size n
   vector<float> z;        // size n
   vector<float> r;        // size n
};

... 이것은 구 중심 벡터 성분과 반경을 동시에 처리하지 않고 (4 개의 필드가 동시에 뜨겁지 않은) 수직 루프 논리에 적합하지만 한 번에 하나씩 (반경을 통한 루프, 다른 3 개의 루프) 구 중심의 개별 구성 요소를 통해).

다른 경우, 필드에 자주 액세스하는 경우 (루프 논리가 개별적으로가 아닌 모든 볼 필드를 반복하는 경우) 및 / 또는 볼의 임의 액세스가 필요한 경우 AoS를 사용하는 것이 더 적절할 수 있습니다.

struct BallAoS
{
    float x;
    float y;
    float z;
    float r;
};
vector<BallAoS> balls;        // size n

... 다른 경우에는 두 이점의 균형을 잡는 하이브리드를 사용하는 것이 적합 할 수 있습니다.

struct BallAoSoA
{
    float x[8];
    float y[8];
    float z[8];
    float r[8];
};
vector<BallAoSoA> balls;      // size n/8

... 공구를 캐시 라인 / 페이지에 맞추기 위해 반 부동을 사용하여 공의 크기를 절반으로 압축 할 수도 있습니다.

struct BallAoSoA16
{
    Float16 x2[16];
    Float16 y2[16];
    Float16 z2[16];
    Float16 r2[16];
};
vector<BallAoSoA16> balls;    // size n/16

... 아마도 반경조차도 구 중심만큼 자주 접근하지는 않을 것입니다 (아마도 코드베이스는 종종 점처럼 점으로 취급하고 구처럼 거의 다루지 않습니다). 이 경우 핫 / 콜드 필드 분할 기술을 더 적용 할 수 있습니다.

struct BallAoSoA16Hot
{
    Float16 x2[16];
    Float16 y2[16];
    Float16 z2[16];
};
vector<BallAoSoA16Hot> balls;     // size n/16: hot fields
vector<Float16> ball_radiuses;    // size n: cold fields

데이터 지향 설계의 핵심은 설계 결정을 내릴 때 초기에 이러한 모든 종류의 표현을 고려하여 공개 인터페이스를 사용하여 차선의 표현에 빠지지 않도록하는 것입니다.

메모리 액세스 패턴과 그에 수반되는 레이아웃에 중점을 두어 평소보다 훨씬 더 큰 관심사를 만듭니다. 어떤 의미에서는 추상화를 다소 분해 할 수도 있습니다. std::deque예를 들어, 알고리즘 요구 사항과 관련하여 집계 된 연속 블록 표현만큼이나 메모리 수준에서 무작위 액세스가 작동하는 방식과 같이 더 이상 보지 않는이 사고 방식을 더 많이 적용 했습니다. 그것은 구현 세부 사항에 다소 초점을 맞추고 있지만 확장 성을 설명하는 알고리즘 복잡성만큼 성능에 거의 영향을 미치는 경향이있는 구현 세부 사항입니다.

조기 최적화

데이터 지향 디자인의 주요 초점은 적어도 한 눈에 조기 최적화에 매우 위험한 것으로 나타납니다. 경험에 따르면 종종 그러한 미세 최적화가 후시와 프로파일 러와 함께 적용되는 것이 가장 좋습니다.

그러나 데이터 지향 설계에서 취해야 할 강력한 메시지는 그러한 최적화를위한 여지를 남겨 두는 것입니다. 이것이 바로 데이터 지향 사고 방식이 도움이되는 것입니다.

데이터 지향 디자인은보다 효과적인 표현을 위해 호흡 공간을 떠날 수 있습니다. 한 번에 메모리 레이아웃을 완벽하게 구현할 필요는 없지만 점점 더 최적화 된 표현을 허용하기 위해 적절한 고려 사항을 미리 만드는 것에 대한 자세한 내용입니다.

세분화 된 객체 지향 디자인

많은 데이터 지향 디자인 토론은 객체 지향 프로그래밍의 고전적인 개념에 맞서 싸울 것입니다. 그러나 나는 OOP를 완전히 무시하는 것만 큼 하드 코어가 아닌 이것을 보는 방법을 제공 할 것입니다.

객체 지향 설계의 어려움은 인터페이스를 매우 세밀한 수준으로 모델링하여 종종 대량 대량 사고 방식 대신 스칼라, 한 번에 한 번의 사고 방식에 갇히게 만들도록 유혹한다는 것입니다.

과장된 예로, 이미지의 단일 픽셀에 적용되는 객체 지향 디자인 사고 방식을 상상해보십시오.

class Pixel
{
public:
    // Pixel operations to blend, multiply, add, blur, etc.

private:
    Image* image;          // back pointer to access adjacent pixels
    unsigned char rgba[4];
};

바라건대 실제로 아무도 이것을하지 않습니다. 예제를 실제로 총체적으로 만들기 위해 픽셀이 포함 된 이미지에 백 포인터를 저장하여 블러와 같은 이미지 처리 알고리즘을 위해 인접 픽셀에 액세스 할 수 있습니다.

이미지 백 포인터는 즉시 눈부신 오버 헤드를 추가하지만, 그것을 배제하더라도 (픽셀의 공용 인터페이스 만 단일 픽셀에 적용되는 작업을 제공) 픽셀을 나타내는 클래스로 끝납니다.

이제이 백 포인터 외에도 C ++ 컨텍스트에서 즉각적인 오버 헤드 의미의 클래스에는 아무런 문제가 없습니다. C ++ 컴파일러 최적화는 우리가 구축 한 모든 구조를 가져 와서 대장간으로 제거하는 데 탁월합니다.

여기서 어려운 점은 캡슐화 된 인터페이스를 너무 세분화 된 픽셀 수준으로 모델링한다는 것입니다. 따라서 우리는 이러한 종류의 세부적인 디자인과 데이터에 갇히게되며이 Pixel인터페이스에 수많은 클라이언트 종속성이 결합 될 수 있습니다.

해결 방법 : 세분화 된 픽셀의 객체 지향 구조를 없애고 대량의 픽셀 (이미지 수준에서)을 다루는 더 거친 수준에서 인터페이스 모델링을 시작하십시오.

벌크 이미지 레벨에서 모델링함으로써 최적화 할 공간이 크게 늘어났습니다. 예를 들어 64 바이트 캐시 라인에 완벽하게 맞지만 일반적으로 작은 보폭으로 픽셀에 효율적으로 인접 수직 액세스 할 수있는 16x16 픽셀의 통합 타일로 큰 이미지를 표현할 수 있습니다 (여러 개의 이미지 처리 알고리즘이있는 경우 하드 코어 데이터 중심의 예제로 세로 방향으로 주변 픽셀에 액세스해야합니다.

더 거친 레벨에서 디자인

이미지 레벨에서의 인터페이스 모델링의 위의 예는 이미지 처리가 연구되고 죽음에 최적화 된 매우 성숙한 분야이기 때문에 일종의 당연한 예입니다. 그러나 입자 방출기, 스프라이트 대 스프라이트 모음, 가장자리 그래프의 모서리, 또는 사람 대 사람 모음에 포함 된 입자가 덜 분명 할 수 있습니다.

데이터 지향 최적화 (예측 또는 후시)를 가능하게하는 핵심은 종종 더 거칠게 인터페이스를 대량으로 설계하는 것입니다. 단일 엔터티에 대한 인터페이스 디자인이라는 개념은 대량으로 처리하는 큰 작업으로 엔터티 모음을 디자인하는 것으로 대체됩니다. 이것은 특히 모든 것에 액세스해야하며 선형 복잡성을 가질 수없는 순차적 액세스 루프를 대상으로합니다.

데이터 지향 설계는 종종 데이터를 통합하여 집계 모델링 데이터를 대량으로 형성하는 아이디어로 시작됩니다. 비슷한 사고 방식이 수반되는 인터페이스 설계에 반영됩니다.

이것은 데이터 지향 디자인에서 얻은 가장 귀중한 교훈입니다. 컴퓨터 아키텍처에 익숙하지 않아서 처음 시도 할 때 가장 최적의 메모리 레이아웃을 찾을 수 없기 때문입니다. 그것은 손에 프로파일 러로 반복되는 무언가가됩니다 (때로는 속도를 올리지 못한 방식으로 몇 번의 실수로). 그러나 데이터 지향 디자인의 인터페이스 디자인 측면에서보다 효율적인 데이터 표현을 찾을 수있는 여지가 있습니다.

핵심은 우리가 일반적으로하고 싶은 것보다 더 거친 레벨에서 인터페이스를 디자인하는 것입니다. 또한 가상 함수, 함수 포인터 호출, dylib 호출과 관련된 동적 디스패치 오버 헤드를 완화하고 인라인 할 수없는 등의 부수적 인 이점도 있습니다. 이 모든 것을 제거하는 주요 아이디어는 대량 처리 (해당되는 경우)로 처리하는 것입니다.


5

설명 한 것은 구현 문제입니다. OO 디자인은 구현과 관련이 없습니다 .

행 중심 또는 열 중심 뷰를 제공하는 인터페이스 뒤에 열 중심 Ball 컨테이너를 캡슐화 할 수 있습니다. volumeand와 같은 메소드를 사용하여 Ball 객체를 구현할 수 있습니다.이 메소드 move는 기본 열 단위 구조에서 각 값을 수정하기 만합니다. 동시에, Ball 컨테이너는 효율적인 컬럼 단위 작업을위한 인터페이스를 노출 할 수 있습니다. 적절한 템플릿 / 타입과 영리한 인라이닝 컴파일러를 사용하면 런타임 비용없이 이러한 추상화를 사용할 수 있습니다.

열 단위로 데이터에 액세스하고 행 단위로 데이터를 수정하는 빈도는? 열 스토리지의 일반적인 사용 사례에서 행 순서는 영향을 미치지 않습니다. 별도의 인덱스 열을 추가하여 행의 임의 순열을 정의 할 수 있습니다. 순서를 변경하면 인덱스 열의 값만 교환하면됩니다.

다른 기술로 요소를 효율적으로 추가 / 제거 할 수 있습니다.

  • 요소를 이동하는 대신 삭제 된 행의 비트 맵을 유지하십시오. 너무 드문 경우 구조를 압축하십시오.
  • 임의의 위치에서 삽입 또는 제거 할 때 전체 구조를 수정할 필요가 없도록 B- 트리 형 구조에서 행을 적절한 크기의 청크로 그룹화하십시오.

클라이언트 코드는 Ball 객체 시퀀스, Ball 객체의 가변 컨테이너, 반경 시퀀스, Nx3 매트릭스 등을 볼 수 있습니다. 복잡한 (그러나 효율적인) 구조의 추악한 세부 사항에 대해서는 신경 쓸 필요가 없습니다. 그것이 객체 추상화가 당신을 사들이는 것입니다.


+1 AoS 조직은 훌륭한 엔티티 지향 API로 완벽하게 수정 가능하지만 의사 포인터를 통해 일관성있는 엔티티를 위조하려는 경우가 아니라면 사용하기가 더 어려워집니다 ( ball->do_something();vs ball_table.do_something(ball)) (&ball_table, index).

1
한 걸음 더 나아갈 것입니다. SoA를 사용한다는 결론은 순수하게 OO 디자인 원칙에서 얻을 수 있습니다. 비결은 열이 행보다 기본 개체 인 시나리오가 필요하다는 것입니다. 여기서 공은 좋은 예가 아닙니다. 대신 높이, 토양 유형 또는 강우와 같은 다양한 특성을 가진 지형을 고려하십시오. 각 속성은 ScalarField 객체로 모델링되며 다른 Field 객체를 반환 할 수있는 gradient () 또는 divergence ()와 같은 자체 메서드가 있습니다. 지도 해상도와 같은 것을 캡슐화 할 수 있으며 지형의 다른 속성이 다른 해상도로 작동 할 수 있습니다.
16807

4

짧은 대답 : 당신은 완전히 정확하고 이와 같은 기사 는이 시점에서 완전히 빠져 있습니다.

전체 대답은 다음과 같습니다. 예제의 "배열 구조"접근 방식은 특정 종류의 작업 ( "열 작업")에 대한 성능 이점과 다른 종류의 작업 ( "행 연산)에 대한 성능 배열"을 가질 수 있습니다. ") (위에서 언급 한 것과 동일) 동일한 원칙이 데이터베이스 아키텍처에 영향을 미쳤으며 열 지향 데이터베이스 와 기존 행 지향 데이터베이스가 있습니다.

따라서 디자인을 선택할 때 고려해야 할 두 번째 사항은 프로그램에서 가장 필요한 작업 유형과 다른 메모리 레이아웃의 이점이 있는지 여부입니다. 그러나 가장 먼저 고려해야 할 것은 그 성능 이 실제로 필요한지 여부 입니다 (게임 프로그래밍에서 위의 기사가 종종 필요합니다).

대부분의 현재 OO 언어는 객체와 클래스에 "Structure-Of-Struct"메모리 레이아웃을 사용합니다. 데이터에 대한 추상화 생성, 캡슐화 및 더 많은 기본 기능의 범위와 같은 OO의 이점을 얻는 것은 일반적으로 이러한 종류의 메모리 레이아웃에 연결됩니다. 따라서 고성능 컴퓨팅을 수행하지 않는 한 SoA를 기본 접근 방식으로 간주하지 않습니다.


3
DOD가 항상 배열 구조 (SoA) 레이아웃을 의미하는 것은 아닙니다. 이 때문에의 일반적인, 종종 다른 레이아웃이 작동 할 때 더 나은, 꼭 액세스 패턴과 일치하지만를 사용합니다. DOD는 데이터를 배치하는 특정 방법보다 설계 패러다임과 같은 훨씬 더 일반적이고 푸지합니다. 또한 참조하는 기사가 최상의 리소스와 거리가 멀고 결함이 있지만 SoA 레이아웃을 알리지 는 않습니다 . 은 "A"s와 "B"의 완전히 기능을 할 수 Ball들은 개별 될 수있는 단지뿐만 아니라의 floats 또는 vec3S (자체 SOA - 변환 대상이 될 것이다).

2
... 그리고 행 지향 디자인은 항상 DOD에 포함됩니다. 이를 AoS (배열 배열)라고하며 대부분의 리소스에서 "OOP 방식"이라고 부르는 것과의 차이점 (행 또는 열)은 행 대 열 레이아웃이 아니라 단순히이 레이아웃이 메모리에 매핑되는 방식 (많은 작은 객체)입니다. 포인터를 통해 연결된 대 모든 레코드의 큰 연속 테이블). 요약하면 -1은 OP의 오해에 대해 좋은 지적을했지만 OP의 DOD에 대한 이해를 수정하는 대신 전체 DOD 재즈를 잘못 표현하기 때문입니다.

@delnan : 귀하의 의견에 감사드립니다. 아마 "DOD"대신 "SoA"라는 용어를 사용해야했을 것입니다. 그에 따라 답변을 편집했습니다.
Doc Brown

훨씬 더 나은 downvote가 제거되었습니다. 훌륭한 "객체"지향 API (추상화, 캡슐화 및 "더 많은 로컬 함수의 기본 범위")로 SoA를 통합 할 수있는 방법에 대한 user2313838의 답변을 확인하십시오. 배열은 요소 유형과 결혼하기보다는 멍청한 일반 컨테이너 일 수 있기 때문에 AoS 레이아웃에 더 자연스럽게 제공되지만 실현 가능합니다.

그리고이 github.com/BSVino/JaiPrimer/blob/master/JaiPrimer.md 는 SoA에서 AoS로 /에서 자동으로 변환됩니다. 예 : reddit.com/r/rust/comments/2t6xqz/… 그리고 뉴스
Jerry Jeremiah
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.