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 Pixel
giao 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).
ball->do_something();
so vớiball_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)
.