Thiết kế hướng dữ liệu - không thực tế với hơn 1-2 cấu trúc Các thành viên của Google?


23

Ví dụ thông thường của Thiết kế hướng dữ liệu là với cấu trúc Ball:

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

và sau đó họ thực hiện một số thuật toán lặp lại một std::vector<Ball>vectơ.

Sau đó, họ cung cấp cho bạn điều tương tự, nhưng được triển khai trong Thiết kế hướng dữ liệu:

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

Điều này là tốt và tất cả nếu bạn định lặp lại tất cả các bán kính trước, sau đó là tất cả các vị trí, v.v. Tuy nhiên, làm thế nào để bạn di chuyển các quả bóng trong vector? Trong phiên bản gốc, nếu bạn có a std::vector<Ball> BallsAll, bạn có thể chuyển bất kỳ BallsAll[x]sang bất kỳ BallsAll[y].

Tuy nhiên, để làm điều đó cho phiên bản Định hướng dữ liệu, bạn phải làm điều tương tự cho mọi thuộc tính (2 lần trong trường hợp Bóng - bán kính và vị trí). Nhưng nó trở nên tồi tệ hơn nếu bạn có nhiều tài sản hơn. Bạn sẽ phải giữ một chỉ số cho mỗi "quả bóng" và khi bạn cố gắng di chuyển nó xung quanh, bạn phải thực hiện di chuyển trong mọi vectơ thuộc tính.

Điều đó không giết chết bất kỳ lợi ích hiệu suất nào của Thiết kế hướng dữ liệu?

Câu trả lời:


23

Một câu trả lời khác đã đưa ra một cái nhìn tổng quan tuyệt vời về cách bạn gói gọn lưu trữ theo hướng hàng và cho một cái nhìn tốt hơn. Nhưng vì bạn cũng hỏi về hiệu suất, hãy để tôi giải quyết rằng: Bố cục SoA không phải là viên đạn bạc . Đó là một mặc định khá tốt (để sử dụng bộ đệm; không quá dễ để thực hiện trong hầu hết các ngôn ngữ), nhưng nó không phải là tất cả, ngay cả trong thiết kế hướng dữ liệu (bất kể điều đó có nghĩa là gì). Có thể các tác giả của một số lời giới thiệu mà bạn đã đọc đã bỏ lỡ điểm đó và chỉ trình bày bố cục SoA vì họ cho rằng đó là toàn bộ quan điểm của DOD. Họ đã sai và rất may không phải ai cũng rơi vào cái bẫy đó .

Như bạn có thể đã nhận ra, không phải mọi phần dữ liệu nguyên thủy đều được hưởng lợi từ việc rút ra trong mảng riêng của nó. Bố cục SoA là lợi thế khi các thành phần mà bạn chia thành các mảng riêng biệt thường được truy cập riêng. Nhưng không phải tất cả các phần nhỏ được truy cập một cách cô lập, ví dụ, một vectơ vị trí hầu như luôn được đọc và cập nhật bán buôn, vì vậy, tự nhiên bạn không tách phần đó. Trong thực tế, ví dụ của bạn đã không làm điều đó! Tương tự như vậy, nếu bạn thường truy cập tất cả các thuộc tính của một quả bóng với nhau, bởi vì bạn dành phần lớn thời gian để trao đổi các quả bóng xung quanh trong bộ sưu tập các quả bóng của bạn, không có lý do gì để tách chúng ra.

Tuy nhiên, có một mặt thứ hai của DOD. Bạn không nhận được tất cả các lợi thế của bộ nhớ cache và tổ chức chỉ bằng cách xoay bố cục bộ nhớ 90 ° và làm ít nhất để sửa các lỗi biên dịch kết quả. Có những thủ thuật phổ biến khác được dạy theo biểu ngữ này. Ví dụ: "xử lý dựa trên tồn tại": Nếu bạn thường xuyên hủy kích hoạt bóng và kích hoạt lại chúng, đừng thêm cờ vào đối tượng bóng và làm cho vòng cập nhật bỏ qua các bóng với cờ được đặt thành sai. Di chuyển quả bóng từ bộ sưu tập "hoạt động" sang bộ sưu tập "không hoạt động" và làm cho vòng cập nhật chỉ kiểm tra bộ sưu tập "hoạt động".

Quan trọng hơn và phù hợp với ví dụ của bạn: Nếu bạn dành quá nhiều thời gian để xáo trộn các quả bóng, có thể bạn đang làm sai điều gì đó. Tại sao thứ tự quan trọng? Bạn có thể làm cho nó không quan trọng? Nếu vậy, bạn sẽ đạt được một số lợi ích:

  • Bạn không cần xáo trộn bộ sưu tập (mã nhanh nhất hoàn toàn không có mã).
  • Bạn có thể thêm và xóa dễ dàng và hiệu quả hơn (trao đổi để kết thúc, thả cuối cùng).
  • Mã còn lại có thể trở thành đủ điều kiện để tối ưu hóa hơn nữa (chẳng hạn như thay đổi bố cục mà bạn tập trung vào).

Vì vậy, thay vì mù quáng ném SoA vào mọi thứ, hãy nghĩ về dữ liệu của bạn và cách bạn xử lý nó. Nếu bạn thấy rằng bạn xử lý các vị trí và vận tốc trong một vòng lặp, sau đó đi qua các mắt lưới, sau đó cập nhật các điểm nhấn, hãy thử chia bố cục bộ nhớ của bạn thành ba phần. Nếu bạn thấy rằng bạn truy cập các thành phần x, y, z của vị trí một cách cô lập, có thể biến các vectơ vị trí của bạn thành SoA. Nếu bạn thấy mình xáo trộn dữ liệu nhiều hơn là thực sự làm điều gì đó hữu ích, có thể ngừng xáo trộn dữ liệu đó.


18

Tư duy định hướng dữ liệu

Thiết kế hướng dữ liệu không có nghĩa là áp dụng SoAs ở mọi nơi. Nó đơn giản có nghĩa là thiết kế kiến ​​trúc với trọng tâm chủ yếu là biểu diễn dữ liệu - đặc biệt tập trung vào bố trí bộ nhớ và truy cập bộ nhớ hiệu quả.

Điều đó có thể có thể dẫn đến đại diện SoA khi thích hợp như vậy:

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

... Điều này thường phù hợp với logic vòng lặp dọc không xử lý đồng thời các thành phần và bán kính vectơ trung tâm hình cầu (bốn trường không nóng đồng thời), nhưng thay vào đó, mỗi lần một vòng (bán kính lặp, 3 vòng lặp khác thông qua các thành phần riêng lẻ của trung tâm hình cầu).

Trong các trường hợp khác, có thể thích hợp hơn khi sử dụng AoS nếu các trường thường xuyên được truy cập cùng nhau (nếu logic vòng lặp của bạn đang lặp qua tất cả các trường của bóng chứ không phải riêng lẻ) và / hoặc nếu cần truy cập ngẫu nhiên vào bóng:

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

... trong các trường hợp khác, có thể phù hợp để sử dụng một phép lai cân bằng cả hai lợi ích:

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

... bạn thậm chí có thể nén kích thước của quả bóng xuống một nửa bằng cách sử dụng một nửa số phao để phù hợp với nhiều trường bóng hơn vào một dòng / trang bộ đệm.

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

... có lẽ ngay cả bán kính cũng không được truy cập gần như trung tâm hình cầu (có lẽ cơ sở mã của bạn thường coi chúng như các điểm và chỉ hiếm khi là hình cầu, ví dụ). Trong trường hợp đó, bạn có thể áp dụng kỹ thuật tách trường nóng / lạnh hơn nữa.

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

Chìa khóa cho thiết kế hướng dữ liệu là sớm xem xét tất cả các loại biểu diễn này trong việc đưa ra quyết định thiết kế của bạn, để không mắc kẹt vào một đại diện phụ tối ưu với giao diện công cộng đằng sau nó.

Nó đặt một điểm sáng trên các mẫu truy cập bộ nhớ và bố cục đi kèm, khiến chúng trở thành mối quan tâm mạnh mẽ hơn đáng kể so với thông thường. Theo một nghĩa nào đó, nó thậm chí có thể phá vỡ sự trừu tượng. Tôi đã tìm thấy bằng cách áp dụng tư duy này nhiều hơn mà tôi không còn nhìn vào std::deque, ví dụ, về các yêu cầu thuật toán của nó cũng giống như biểu diễn các khối liền kề tổng hợp mà nó có và cách truy cập ngẫu nhiên của nó hoạt động ở cấp độ bộ nhớ. Nó phần nào tập trung vào các chi tiết triển khai, nhưng các chi tiết triển khai có xu hướng ảnh hưởng nhiều đến hiệu suất cũng như độ phức tạp thuật toán mô tả khả năng mở rộng.

Tối ưu hóa sớm

Rất nhiều trọng tâm chính của thiết kế hướng dữ liệu sẽ xuất hiện, ít nhất là trong nháy mắt, gần như nguy hiểm gần với tối ưu hóa sớm. Kinh nghiệm thường dạy chúng ta rằng tối ưu hóa vi mô như vậy được áp dụng tốt nhất trong nhận thức muộn, và với một hồ sơ trong tay.

Tuy nhiên, có lẽ một thông điệp mạnh mẽ để lấy từ thiết kế hướng dữ liệu là chừa chỗ cho những tối ưu hóa như vậy. Đó là những gì một tư duy định hướng dữ liệu có thể giúp cho phép:

Thiết kế hướng dữ liệu có thể rời khỏi phòng thở để khám phá các biểu diễn hiệu quả hơn. Không nhất thiết phải đạt được sự hoàn hảo về bố cục bộ nhớ trong một lần, mà nhiều hơn là đưa ra những cân nhắc phù hợp trước để cho phép các biểu diễn ngày càng tối ưu.

Thiết kế hướng đối tượng dạng hạt

Rất nhiều cuộc thảo luận về thiết kế hướng dữ liệu sẽ tự chống lại các quan niệm cổ điển về lập trình hướng đối tượng. Tuy nhiên, tôi sẽ đưa ra một cách nhìn về điều này không quá khó để loại bỏ hoàn toàn OOP.

Khó khăn với thiết kế hướng đối tượng là nó thường sẽ cám dỗ chúng ta mô hình hóa các giao diện ở mức rất chi tiết, khiến chúng ta bị mắc kẹt với một suy nghĩ vô hướng, một lúc thay vì một tư duy hàng loạt song song.

Như một ví dụ phóng đại, hãy tưởng tượng một tư duy thiết kế hướng đối tượng được áp dụng cho một pixel của hình ảnh.

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

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

Hy vọng không ai thực sự làm điều này. Để làm cho ví dụ thực sự thô thiển, tôi đã lưu trữ một con trỏ trở lại hình ảnh chứa pixel để nó có thể truy cập các pixel lân cận cho các thuật toán xử lý hình ảnh như mờ.

Con trỏ trở lại hình ảnh ngay lập tức thêm một chi phí rõ ràng, nhưng ngay cả khi chúng tôi loại trừ nó (chỉ giao diện công khai của pixel cung cấp các hoạt động áp dụng cho một pixel), chúng tôi kết thúc với một lớp chỉ để biểu thị một pixel.

Bây giờ không có gì sai với một lớp theo nghĩa trực tiếp ngay lập tức trong ngữ cảnh C ++ bên cạnh con trỏ trở lại này. Tối ưu hóa trình biên dịch C ++ rất tốt trong việc lấy tất cả cấu trúc chúng tôi xây dựng và xóa sạch nó xuống smithereens.

Khó khăn ở đây là chúng ta đang mô hình hóa một giao diện được đóng gói ở mức độ chi tiết của mức pixel. Điều đó khiến chúng ta bị mắc kẹt với kiểu thiết kế và dữ liệu dạng hạt này, với khả năng rất nhiều phụ thuộc khách hàng ghép chúng với Pixelgiao diện này .

Giải pháp: xóa sạch cấu trúc hướng đối tượng của pixel pixel và bắt đầu mô hình hóa các giao diện của bạn ở mức độ thô hơn xử lý một số lượng lớn pixel (ở cấp độ hình ảnh).

Bằng cách mô hình hóa ở mức hình ảnh số lượng lớn, chúng tôi có nhiều chỗ hơn để tối ưu hóa. Ví dụ, chúng ta có thể biểu diễn các hình ảnh lớn dưới dạng các khối kết hợp 16x16 pixel hoàn toàn phù hợp với dòng bộ đệm 64 byte nhưng cho phép truy cập các pixel dọc lân cận hiệu quả với một bước tiến nhỏ (nếu chúng ta có một số thuật toán xử lý hình ảnh cần truy cập các pixel lân cận theo kiểu dọc) như một ví dụ hướng dữ liệu cứng.

Thiết kế ở cấp độ Coarser

Ví dụ trên về mô hình giao diện ở mức hình ảnh là một ví dụ không có trí tuệ vì xử lý hình ảnh là một lĩnh vực rất trưởng thành được nghiên cứu và tối ưu hóa cho đến chết. Nhưng ít rõ ràng hơn có thể là một hạt trong một bộ phát hạt, một sprite so với một tập hợp các sprite, một cạnh trong biểu đồ các cạnh hoặc thậm chí là một người so với một tập hợp người.

Chìa khóa để cho phép tối ưu hóa theo định hướng dữ liệu (trong tầm nhìn xa hoặc tầm nhìn xa) thường sẽ đi sâu vào việc thiết kế các giao diện ở mức độ thô hơn, đồng loạt. Ý tưởng thiết kế giao diện cho các thực thể đơn lẻ được thay thế bằng cách thiết kế cho các bộ sưu tập của các thực thể với các hoạt động lớn xử lý chúng hàng loạt. Điều này đặc biệt và ngay lập tức nhắm mục tiêu các vòng truy cập tuần tự cần truy cập mọi thứ và không thể giúp đỡ nhưng có độ phức tạp tuyến tính.

Thiết kế hướng dữ liệu thường bắt đầu với ý tưởng kết hợp dữ liệu để tạo thành dữ liệu mô hình hóa tổng hợp hàng loạt. Một suy nghĩ tương tự lặp lại với các thiết kế giao diện đi kèm với nó.

Đây là bài học quý giá nhất tôi học được từ thiết kế hướng dữ liệu, vì tôi không đủ hiểu biết về kiến ​​trúc máy tính để thường tìm bố cục bộ nhớ tối ưu nhất cho lần thử đầu tiên. Nó trở thành thứ gì đó tôi lặp đi lặp lại với một hồ sơ trong tay (và đôi khi có một vài lần bỏ lỡ dọc đường mà tôi không thể tăng tốc mọi thứ). Tuy nhiên, khía cạnh thiết kế giao diện của thiết kế hướng dữ liệu là điều khiến tôi có thể tìm kiếm các biểu diễn dữ liệu ngày càng hiệu quả hơn.

Điều quan trọng là thiết kế giao diện ở mức độ thô hơn chúng ta thường muốn làm. Điều này cũng thường có các lợi ích phụ như giảm thiểu chi phí công văn động liên quan đến các chức năng ảo, các cuộc gọi con trỏ hàm, các cuộc gọi dylib và không có khả năng cho những người được nội tuyến. Ý tưởng chính để loại bỏ tất cả những điều này là xem xét xử lý theo cách thức số lượng lớn (khi áp dụng).


5

Những gì bạn đã mô tả là một vấn đề thực hiện. Thiết kế OO rõ ràng không liên quan đến việc thực hiện.

Bạn có thể gói gọn Hộp chứa bóng hướng cột của mình phía sau giao diện hiển thị chế độ xem theo hàng hoặc theo cột. Bạn có thể triển khai một đối tượng Ball với các phương thức như volumemove, chỉ đơn thuần là sửa đổi các giá trị tương ứng trong cấu trúc cột thông minh bên dưới. Đồng thời, thùng chứa Ball của bạn có thể hiển thị giao diện cho các hoạt động theo cột hiệu quả. Với các mẫu / loại thích hợp và trình biên dịch nội tuyến thông minh, bạn có thể sử dụng các tóm tắt này với chi phí thời gian chạy bằng không.

Tần suất bạn sẽ truy cập cột dữ liệu so với sửa đổi hàng khôn ngoan? Trong các trường hợp sử dụng thông thường để lưu trữ cột, thứ tự của các hàng không có hiệu lực. Bạn có thể xác định một hoán vị tùy ý của các hàng bằng cách thêm một cột chỉ mục riêng. Thay đổi thứ tự sẽ chỉ yêu cầu trao đổi giá trị trong cột chỉ mục.

Hiệu quả bổ sung / loại bỏ các yếu tố có thể đạt được với các kỹ thuật khác:

  • Duy trì một bitmap của các hàng bị xóa thay vì các yếu tố thay đổi. Nén cấu trúc khi nó quá thưa thớt.
  • Nhóm các hàng thành các khối có kích thước phù hợp trong cấu trúc giống như B-Tree để việc chèn hoặc loại bỏ ở các vị trí tùy ý không yêu cầu sửa đổi toàn bộ cấu trúc.

Mã khách hàng sẽ thấy một chuỗi các đối tượng Ball, một thùng chứa các đối tượng Ball có thể thay đổi, một chuỗi bán kính, ma trận Nx3, v.v; không cần phải quan tâm đến các chi tiết xấu xí của các cấu trúc phức tạp (nhưng hiệu quả) đó. Đó là những gì đối tượng trừu tượng mua cho bạn.


Tổ chức AoS +1 hoàn toàn có thể sửa đổi thành API hướng thực thể đẹp, mặc dù phải thừa nhận rằng nó trở nên xấu hơn khi sử dụng ( ball->do_something();so với ball_table.do_something(ball)) trừ khi bạn muốn giả mạo một thực thể kết hợp thông qua con trỏ giả (&ball_table, index).

1
Tôi sẽ tiến thêm một bước: Kết luận sử dụng SoA hoàn toàn có thể đạt được từ các nguyên tắc thiết kế OO. Bí quyết là bạn cần một kịch bản trong đó các cột là một đối tượng cơ bản hơn các hàng. Balls không phải là một ví dụ tốt ở đây. Thay vào đó, hãy xem xét một địa hình có các tính chất khác nhau như chiều cao, loại đất hoặc lượng mưa. Mỗi thuộc tính được mô hình hóa như một đối tượng ScalarField, có các phương thức riêng như gradient () hoặc phân kỳ () có thể trả về các đối tượng Trường khác. Bạn có thể gói gọn những thứ như độ phân giải bản đồ và các thuộc tính khác nhau trên địa hình có thể hoạt động với các độ phân giải khác nhau.
16807

4

Câu trả lời ngắn gọn: bạn hoàn toàn chính xác, và những bài viết như thế này hoàn toàn thiếu điểm này.

Câu trả lời đầy đủ là: cách tiếp cận "Cấu trúc của các mảng" trong các ví dụ của bạn có thể có lợi thế về hiệu suất cho một số loại hoạt động ("hoạt động cột") và "Mảng hoạt động" cho các loại hoạt động khác ("hoạt động hàng ", Giống như những người bạn đã đề cập ở trên). Nguyên tắc tương tự đã ảnh hưởng đến kiến ​​trúc cơ sở dữ liệu, có cơ sở dữ liệu hướng cột so với cơ sở dữ liệu hướng hàng cổ điển

Vì vậy, điều thứ hai cần xem xét để chọn một thiết kế là loại hoạt động nào bạn cần nhất trong chương trình của bạn và nếu chúng sẽ được hưởng lợi từ cách bố trí bộ nhớ khác nhau. Tuy nhiên, điều đầu tiên cần xem xét là nếu bạn thực sự cần hiệu suất đó (tôi nghĩ trong lập trình trò chơi, trong đó bài viết trên là của bạn thường có yêu cầu này).

Hầu hết các ngôn ngữ OO hiện tại sử dụng bố trí bộ nhớ "Array-Of-Struct" cho các đối tượng và lớp. Có được những lợi thế của OO (như tạo ra sự trừu tượng cho dữ liệu của bạn, đóng gói và phạm vi địa phương hơn của các hàm cơ bản), thường được liên kết với kiểu bố trí bộ nhớ này. Vì vậy, miễn là bạn không làm điện toán hiệu năng cao, tôi sẽ không coi SoA là phương pháp chính.


3
DOD không phải lúc nào cũng có nghĩa là bố cục Cấu trúc của mảng (SoA). Nó phổ biến, bởi vì nó thường phù hợp với mẫu truy cập, nhưng khi một bố cục khác hoạt động tốt hơn, bằng mọi cách, hãy sử dụng nó. DOD là một tổng quát hơn (và mờ hơn), giống như một mô hình thiết kế hơn là một cách cụ thể để bố trí dữ liệu. Ngoài ra, trong khi bài viết bạn tham khảo cách xa tài nguyên tốt nhất và có những sai sót, nó không quảng cáo bố cục SoA. Các chữ "A" và "B" có thể là các tính năng đầy đủ Ballcũng như chúng có thể là các cá nhân floathoặc chính vec3chúng (chúng sẽ chịu sự biến đổi SoA).

2
... Và thiết kế hướng hàng mà bạn đề cập luôn được bao gồm trong DOD. Nó được gọi là một mảng các cấu trúc (AoS) và sự khác biệt so với hầu hết các tài nguyên gọi là "cách OOP" (để tốt hơn hoặc tốt hơn) không theo hàng so với bố cục cột mà chỉ đơn giản là cách bố trí này được ánh xạ vào bộ nhớ (nhiều đối tượng nhỏ liên kết thông qua con trỏ so với bảng liên tục lớn của tất cả các bản ghi). Tóm lại, -1 vì mặc dù bạn nêu lên những điểm tốt chống lại những quan niệm sai lầm của OP, nhưng bạn đã trình bày sai về toàn bộ nhạc jazz DOD thay vì sửa chữa sự hiểu biết về DOD của OP.

@delnan: cảm ơn vì nhận xét của bạn, có lẽ bạn đã đúng rằng tôi nên sử dụng thuật ngữ "SoA" thay vì "DOD". Tôi chỉnh sửa câu trả lời của tôi cho phù hợp.
Doc Brown

Tốt hơn nhiều, downvote loại bỏ. Kiểm tra câu trả lời của user2313838 để biết cách SoA có thể được hợp nhất với các API định hướng "đối tượng" đẹp (theo nghĩa trừu tượng, đóng gói và "phạm vi địa phương hơn của các hàm cơ bản"). Nó xuất hiện một cách tự nhiên hơn cho bố cục AoS (vì mảng có thể là một thùng chứa chung câm thay vì kết hôn với loại phần tử) nhưng điều đó là khả thi.

github.com/BSVino/JaiPrimer/blob/master/JaiPrimer.md này đã tự động chuyển đổi từ SoA sang / từ AoS Ví dụ: reddit.com/r/rust/comments/2t6xqz/ và sau đó có tin
Jerry Jeremiah
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.