Để tự động liên kết và phân tách dữ liệu một cách độc lập với vòng đời của nút QT trong khi QT kết hợp với máy ảnh có kiến thức về thời điểm dữ liệu nên được liên kết / phân tách khi đang di chuyển, điều đó hơi khó để khái quát hóa và tôi nghĩ rằng giải pháp của bạn là thực ra không tệ Đó là một điều khó khăn để thiết kế theo một cách rất hay và khái quát. kiểu như, "uhh .... kiểm tra nó tốt và vận chuyển nó!" Được rồi, một chút của một trò đùa. Tôi sẽ cố gắng đưa ra một vài suy nghĩ để khám phá. Một trong những điều làm tôi kinh ngạc nhất là ở đây:
void nodeCreated(Node& node)
{
...
// One more thing, The QuadTree actually needs one field of
// Data to continue, so I fill it there
node.xxx = data.xxx
}
Điều này cho tôi biết rằng một nút ref / con trỏ không chỉ được sử dụng làm khóa vào một thùng chứa kết hợp bên ngoài. Bạn đang thực sự truy cập và sửa đổi các phần bên trong của nút tứ giác bên ngoài chính tứ giác. Và nên có một cách khá dễ dàng để ít nhất là tránh điều đó cho người mới bắt đầu. Nếu đó là nơi duy nhất mà bạn sửa đổi các nút bên trong bên ngoài tứ giác, thì bạn có thể làm điều này (giả sử xxx
là một cặp phao):
std::pair<float, float> nodeCreated(const Node& node)
{
Data data;
...
map[&node] = data;
...
return data.xxx;
}
Tại điểm đó, tứ giác có thể sử dụng giá trị trả về của hàm này để gán xxx
. Điều đó đã nới lỏng khớp nối khá nhiều khi bạn không còn truy cập vào bên trong của một nút cây bên ngoài cây.
Loại bỏ sự cần thiết phải Terrain
truy cập vào nội bộ mười bốn sẽ thực sự loại bỏ nơi duy nhất mà bạn ghép những thứ rất không cần thiết. Ví dụ, đó là PITA thực sự nếu bạn trao đổi mọi thứ với việc triển khai GPU, vì việc triển khai GPU có thể sử dụng một đại diện nội bộ hoàn toàn khác cho các nút.
Nhưng đối với mối quan tâm về hiệu suất của bạn, và ở đó tôi có nhiều suy nghĩ hơn là cách bạn đạt được sự tách rời tối đa với loại điều này, tôi thực sự sẽ đề xuất một đại diện rất khác trong đó bạn có thể biến liên kết / phân tách dữ liệu thành hoạt động liên tục thời gian rẻ tiền. Thật khó để giải thích với ai đó không quen với việc xây dựng các thùng chứa tiêu chuẩn yêu cầu vị trí mới để xây dựng các phần tử thay thế từ bộ nhớ gộp nên tôi sẽ bắt đầu với một số dữ liệu:
struct Node
{
....
// Stores an index to the data being associated on the fly
// or -1 if there's no data associated to the node.
int32_t data;
};
class Quadtree
{
private:
// Stores all the data being associated on the fly.
std::vector<char> data;
// Stores the size of the data being associated on the fly.
int32_t type_size;
// Stores an index to the first free index of data
// to reclaim or -1 if the free list is empty.
int32_t free_index;
...
public:
// Creates a quadtree with the specified type size for the
// data associated and disassociated on the fly.
explicit Quadtree(int32_t itype_size): type_size(itype_size), free_data(-1)
{
// Make sure our data type size is at least the size of an integer
// as required for the free list.
if (type_size < sizeof(int32_t))
type_size = sizeof(int32_t);
}
// Inserts a buffer to store a data element and returns an index
// to that.
int32_t alloc_data()
{
int32_t index = free_index;
if (free_index != -1)
{
// If a free index is available, pop it off the
// free list (stack) and return that.
void* mem = data.data() + index * type_size;
free_index = *static_cast<int*>mem;
}
else
{
// Otherwise insert the buffer for the data
// and return an index to that.
index = data.size() / type_size;
data.resize(data.size() + type_size);
}
return index;
}
// Frees the memory for the nth data element.
void free_data(int32_t n)
{
// Push the nth index to the free list to make
// it available for use in subsequent insertions.
void* mem = data.data() + n * type_size;
*static_cast<int*>(mem) = free_index;
free_index = n;
}
...
};
Về cơ bản đó là một "danh sách miễn phí được lập chỉ mục". Nhưng khi bạn sử dụng đại diện này cho dữ liệu liên quan, bạn có thể làm một cái gì đó như thế này:
class QTInterface
{
virtual std::pair<float, float> createData(void* mem) = 0;
virtual void destroyData(void* mem) = 0;
};
void Quadtree::update(Camera camera)
{
...
node.data = alloc_data();
node.xxx = i.createData(data.data() + node.data * type_size);
...
i.destroyData(data.data() + node.data * type_size);
free_data(node.data);
node.data = -1;
...
}
class Terrain : public QTInterface
{
// Note that we don't even need access to nodes anymore,
// not even as keys to use. We've completely decoupled
// terrains from tree internals.
std::pair<float, float> createData(void* mem) override
{
// Construct the data (placement new) using the memory
// allocated by the tree.
Data* data = new(mem) Data(...);
// Return data to assign to node.xxx.
return data->xxx;
}
void destroyData(void* mem) override
{
// Destroy the data.
static_cast<Data*>(mem)->~Data();
}
};
Hy vọng tất cả điều này có ý nghĩa, và tự nhiên nó tách rời hơn một chút so với thiết kế ban đầu của bạn vì nó không yêu cầu khách hàng có quyền truy cập nội bộ vào các trường nút cây (bây giờ thậm chí không còn cần kiến thức về các nút, thậm chí không sử dụng làm khóa ) và nó hiệu quả hơn đáng kể vì bạn có thể liên kết và phân tách dữ liệu đến / từ các nút trong thời gian không đổi (và không sử dụng bảng băm có nghĩa là hằng số lớn hơn nhiều). Tôi hy vọng dữ liệu của bạn có thể được căn chỉnh bằng cách sử dụng max_align_t
(không có trường SIMD, ví dụ) và có thể sao chép một cách tầm thường, nếu không mọi thứ sẽ phức tạp hơn đáng kể vì chúng tôi cần một bộ cấp phát được căn chỉnh và có thể phải cuộn hộp chứa danh sách miễn phí của riêng chúng tôi. Chà, nếu bạn chỉ có những loại không thể sao chép và không cần nhiều hơnmax_align_t
, chúng ta có thể sử dụng triển khai con trỏ danh sách miễn phí, tập hợp và liên kết các nút không được kiểm soát lưu trữ K
từng phần tử dữ liệu để tránh phải phân bổ lại các khối bộ nhớ hiện có. Tôi có thể chỉ ra rằng nếu bạn cần một sự thay thế như vậy.
Nó hơi tiên tiến và rất cụ thể về C ++, xem xét ý tưởng phân bổ và giải phóng bộ nhớ cho các phần tử như một nhiệm vụ riêng biệt từ việc xây dựng và phá hủy chúng. Nhưng nếu bạn làm theo cách này, tiếp Terrain
thu các trách nhiệm tối thiểu và không đòi hỏi kiến thức nội bộ về đại diện của cây nữa, thậm chí không xử lý các nút mờ. Tuy nhiên, mức độ kiểm soát bộ nhớ này thường là những gì bạn cần nếu bạn muốn thiết kế cấu trúc dữ liệu hiệu quả nhất.
Ý tưởng cơ bản là bạn có ứng dụng khách sử dụng cây thông qua kích thước loại dữ liệu mà họ muốn liên kết / phân tách khi đang bay đến ctor tứ giác. Sau đó, bộ tứ có trách nhiệm phân bổ và giải phóng bộ nhớ bằng cách sử dụng kích thước loại đó. Sau đó, nó chuyển giao trách nhiệm xây dựng và hủy dữ liệu cho khách hàng bằng cách sử dụng QTInterface
và gửi động. Do đó, trách nhiệm duy nhất, bên ngoài cây vẫn liên quan đến cây, là xây dựng và phá hủy các yếu tố khỏi bộ nhớ mà bộ tứ phân bổ và tự giải quyết. Tại thời điểm đó, các phụ thuộc trở nên như thế này:
Điều này rất hợp lý khi xem xét độ khó của những gì bạn đang làm và quy mô của các yếu tố đầu vào. Về cơ bản Terrain
thì sau đó của bạn chỉ phụ thuộc vào Quadtree
và QTInterface
, và không còn là phần bên trong của tứ giác hoặc các nút của nó. Trước đây bạn đã có điều này:
Và tất nhiên một vấn đề trừng mắt với điều đó, đặc biệt là nếu bạn đang cân nhắc thử cài GPU, là phụ thuộc từ Terrain
để Node
, vì một thực GPU có khả năng sẽ muốn sử dụng một đại diện nút rất khác nhau. Tất nhiên, nếu bạn muốn đi RẮN RẮN, bạn sẽ làm một cái gì đó như thế này:
... Cùng với có thể là một nhà máy. Nhưng IMO đó là quá mức cần thiết (ít nhất INode
là tổng IMO quá mức cần thiết) và sẽ không hữu ích trong trường hợp chi tiết như một hàm tứ giác nếu mỗi yêu cầu một công văn động.
Tôi luôn có một khoảng thời gian khó khăn với cách phân tách các lớp học của mình một cách chính xác. Có lời khuyên nào để tôi có thể sử dụng sau này không? .
Nói một cách chung chung và thô lỗ, việc tách rời thường tập trung vào việc giới hạn lượng thông tin mà một lớp hoặc hàm cụ thể yêu cầu về một thứ khác để thực hiện công việc của mình.
Tôi giả sử bạn đang sử dụng C ++ vì không có ngôn ngữ nào khác mà tôi biết có cú pháp chính xác đó và trong C ++, một cơ chế tách rời rất hiệu quả cho các cấu trúc dữ liệu là các mẫu lớp với đa hình tĩnh nếu bạn có thể sử dụng chúng. Nếu bạn xem xét các thùng chứa tiêu chuẩn như thế std::vector<T, Alloc>
, vectơ không được ghép với bất cứ thứ gì bạn chỉ định cho T
bất cứ điều gì. Nó chỉ yêu cầu T
đáp ứng một số yêu cầu giao diện cơ bản như nó có thể sao chép được và có một hàm tạo mặc định cho hàm tạo và điền thay đổi kích thước. Và nó sẽ không bao giờ yêu cầu thay đổi do kết quả của T
việc thay đổi.
Vì vậy, việc buộc nó vào phần trên, nó cho phép thực hiện cấu trúc dữ liệu bằng cách sử dụng kiến thức tối thiểu tuyệt đối về những gì nó chứa, và điều đó tách rời nó đến mức mà nó thậm chí không cần bất kỳ thông tin loại nào trước (trước đây là nói về các phụ thuộc mã / khớp nối, không phải thông tin thời gian biên dịch) về những gì T
là.
Cách thực tế thứ hai để giảm thiểu lượng thông tin cần thiết là sử dụng đa hình động. Ví dụ: nếu bạn muốn triển khai cấu trúc dữ liệu tổng quát hợp lý nhằm giảm thiểu kiến thức về những gì nó lưu trữ, thì bạn có thể nắm bắt các yêu cầu giao diện cho những gì nó lưu trữ trong một hoặc nhiều giao diện:
// Contains all the functions (pure virtual) required of the elements
// stored in the container.
class IElement {...};
Nhưng dù bằng cách nào thì nó cũng sẽ giảm thiểu lượng thông tin bạn cần trước bằng cách mã hóa các giao diện thay vì các chi tiết cụ thể. Ở đây, điều lớn duy nhất bạn đang làm dường như đòi hỏi nhiều thông tin hơn mức yêu cầu là bạn Terrain
phải có thông tin đầy đủ về các phần bên trong của nút Quadtree, ví dụ như trong trường hợp như vậy, giả sử lý do duy nhất bạn cần đó là để gán một phần dữ liệu cho một nút, chúng ta có thể dễ dàng loại bỏ sự phụ thuộc đó vào phần bên trong của nút cây bằng cách chỉ trả về dữ liệu cần được gán cho nút trong bản tóm tắt đó QTInterface
.
Vì vậy, nếu tôi muốn tách rời một cái gì đó, tôi chỉ tập trung vào những gì nó cần để làm mọi thứ và đưa ra một giao diện cho nó (có thể sử dụng kế thừa hoặc ẩn bằng cách sử dụng đa hình tĩnh và gõ vịt). Và bạn đã làm điều đó ở một mức độ nào đó từ chính tứ giác bằng cách sử dụng QTInterface
để cho phép khách hàng ghi đè các chức năng của nó bằng một kiểu con và cung cấp các chi tiết cụ thể cần thiết cho tứ giác để thực hiện công việc của mình. Nơi duy nhất mà tôi nghĩ rằng bạn cảm thấy hụt hẫng là khách hàng vẫn yêu cầu quyền truy cập vào nội bộ của tứ giác. Bạn có thể tránh điều đó bằng cách tăng những gì QTInterface
chính xác là những gì tôi đã đề xuất khi tôi thực hiện nó trả về một giá trị được gán chonode.xxx
trong việc thực hiện tứ giác chính nó. Vì vậy, đó chỉ là vấn đề làm cho mọi thứ trở nên trừu tượng hơn và giao diện hoàn thiện hơn để mọi thứ không yêu cầu thông tin không cần thiết về nhau.
Và bằng cách tránh thông tin không cần thiết đó ( Terrain
phải biết về các Quadtree
nút bên trong), giờ đây bạn có thể tự do trao đổi Quadtree
với việc triển khai GPU, mà không thay đổi việc Terrain
triển khai. Những điều không biết về nhau là tự do thay đổi mà không ảnh hưởng đến nhau. Nếu bạn thực sự muốn trao đổi các triển khai của bộ tứ GPU từ CPU, bạn có thể đi một chút về tuyến đường RẮN ở trên vớiIQuadtree
(làm cho tứ giác tự trừu tượng). Điều đó đi kèm với một cú đánh chuyển động có thể tốn kém một chút với độ sâu của cây và kích thước đầu vào mà bạn đang nói đến. Nếu không, ít nhất nó đòi hỏi ít thay đổi hơn về mã nếu những thứ sử dụng tứ giác không cần phải biết về biểu diễn nút bên trong của nó để hoạt động. Bạn có thể trao đổi cái này với cái kia chỉ bằng cách cập nhật một dòng mã cho một typedef
, ví dụ, ngay cả khi bạn không sử dụng giao diện trừu tượng ( IQuadtree
).
Nhưng đó là nơi tôi nghĩ rằng tôi có vấn đề đầu tiên của tôi. Hầu hết thời gian tôi không lo lắng về việc tối ưu hóa cho đến khi tôi nhìn thấy nó, nhưng tôi nghĩ rằng nếu tôi phải thêm loại chi phí này để tách lớp của tôi đúng cách, thì đó là do thiết kế có lỗi.
Không cần thiết. Decoupling thường ngụ ý chuyển một sự phụ thuộc từ cụ thể sang trừu tượng. Trừu tượng có xu hướng ngụ ý một hình phạt thời gian chạy trừ khi trình biên dịch đang tạo mã tại thời gian biên dịch để cơ bản loại bỏ chi phí trừu tượng hóa trong thời gian chạy. Đổi lại, bạn sẽ có nhiều phòng thở hơn để thực hiện các thay đổi mà không ảnh hưởng đến những thứ khác, nhưng điều đó thường trích xuất một số loại hình phạt hiệu suất trừ khi bạn đang sử dụng tạo mã.
Bây giờ bạn có thể loại bỏ nhu cầu về cấu trúc dữ liệu liên kết không tầm thường (map / dictionary, nghĩa là) để liên kết dữ liệu với các nút (hoặc bất cứ thứ gì khác) một cách nhanh chóng. Trong trường hợp trên, tôi chỉ thực hiện các nút lưu trữ trực tiếp một chỉ mục cho dữ liệu được phân bổ / giải phóng nhanh chóng. Thực hiện các loại điều này không liên quan nhiều đến việc nghiên cứu cách tách rời mọi thứ một cách hiệu quả cũng như cách sử dụng bố cục bộ nhớ cho các cấu trúc dữ liệu một cách hiệu quả (nhiều hơn trong lĩnh vực tối ưu hóa thuần túy).
Các nguyên tắc và hiệu suất SE hiệu quả là bất hòa với nhau ở mức đủ thấp. Việc tách riêng thường sẽ phân chia bố cục bộ nhớ cho các trường thường được truy cập cùng nhau, có thể liên quan đến phân bổ heap nhiều hơn, có thể liên quan đến việc gửi động hơn, v.v. Nó sẽ nhanh chóng bị tầm thường hóa khi bạn làm việc với mã cấp cao hơn (ví dụ: các thao tác áp dụng cho toàn bộ hình ảnh, không phải cho mỗi Hoạt động-pixel khi lặp qua các pixel riêng lẻ), nhưng nó có chi phí dao động từ tầm thường đến nghiêm trọng tùy thuộc vào mức chi phí phát sinh trong mã quan trọng nhất của bạn, thực hiện công việc nhẹ nhất trong mỗi lần lặp.
Tôi có quá phức tạp không? Tôi có nên mở rộng lớp Node, biến nó thành một túi dữ liệu được sử dụng bởi một số lớp không?
Cá nhân tôi không nghĩ điều đó quá tệ nếu bạn không cố gắng khái quát hóa cấu trúc dữ liệu của mình quá nhiều, chỉ sử dụng nó trong một bối cảnh rất hạn chế và bạn đang xử lý một bối cảnh cực kỳ quan trọng về hiệu năng cho một loại vấn đề mà bạn gặp phải đã giải quyết trước đây. Trong trường hợp đó, bạn sẽ biến tứ giác của mình thành một chi tiết triển khai địa hình của mình, ví dụ, thay vì sử dụng rộng rãi và công khai, theo cách tương tự ai đó có thể biến một quãng tám thành một chi tiết thực hiện của công cụ vật lý của họ bằng cách không còn phân biệt ý tưởng về "giao diện công cộng" từ "nội bộ". Việc duy trì các bất biến liên quan đến chỉ số không gian sau đó biến thành trách nhiệm của lớp sử dụng nó như một chi tiết thực hiện riêng tư.
Để thiết kế một sự trừu tượng hóa hiệu quả (giao diện, tức là) trong bối cảnh quan trọng về hiệu năng thường đòi hỏi bạn phải hiểu thấu đáo phần lớn vấn đề và một giải pháp rất hiệu quả cho việc trả trước. Nó thực sự có thể biến thành một biện pháp phản tác dụng để cố gắng khái quát hóa và trừu tượng hóa giải pháp trong khi đồng thời cố gắng tìm ra thiết kế hiệu quả qua nhiều lần lặp. Một trong những lý do là bối cảnh quan trọng về hiệu năng đòi hỏi các biểu diễn dữ liệu và mẫu truy cập rất hiệu quả. Các tóm tắt đặt một rào cản giữa mã muốn truy cập dữ liệu: một rào cản hữu ích nếu bạn muốn dữ liệu được tự do thay đổi mà không ảnh hưởng đến mã đó, nhưng trở ngại nếu bạn cố gắng tìm ra cách hiệu quả nhất để thể hiện và truy cập dữ liệu đó ở nơi đầu tiên.
Nhưng nếu bạn làm theo cách này, một lần nữa tôi sẽ đứng về phía biến tứ giác thành một chi tiết triển khai riêng tư về địa hình của bạn, không phải là thứ gì đó được khái quát và sử dụng bên ngoài các triển khai của chúng. Và bạn phải từ bỏ ý tưởng có thể dễ dàng trao đổi việc triển khai GPU từ việc triển khai CPU, vì điều đó thường đòi hỏi phải có một sự trừu tượng hoạt động cho cả hai và không trực tiếp phụ thuộc vào chi tiết cụ thể (như đại diện nút) của một trong hai.
Điểm tách rời
Nhưng có thể trong một số trường hợp, điều này thậm chí có thể được chấp nhận cho những thứ được sử dụng công khai hơn. Trước khi mọi người nghĩ rằng tôi đang nói những điều vô nghĩa, hãy xem xét các giao diện hình ảnh. Có bao nhiêu trong số chúng đủ cho một bộ xử lý video cần áp dụng các bộ lọc hình ảnh trên video trong thời gian thực nếu hình ảnh không lộ nội bộ của nó (truy cập trực tiếp vào mảng pixel bên dưới của nó ở định dạng pixel cụ thể)? Không có cái nào tôi biết sử dụng một cái gì đó như trừu tượng / ảo getPixel
ở đây vàsetPixel
ở đó trong khi thực hiện chuyển đổi định dạng pixel trên cơ sở mỗi pixel. Vì vậy, trong các bối cảnh quan trọng về hiệu năng, trong đó bạn phải truy cập mọi thứ ở mức rất chi tiết (mỗi pixel, mỗi nút, v.v.), đôi khi bạn có thể phải phơi bày phần bên trong của cấu trúc bên dưới. Nhưng chắc chắn bạn sẽ phải kết hợp chặt chẽ mọi thứ và kết quả là sẽ không dễ dàng thay đổi cách thể hiện cơ bản của hình ảnh (thay đổi định dạng hình ảnh, ví dụ), có thể nói, mà không ảnh hưởng đến mọi thứ truy cập vào các pixel bên dưới. Nhưng có thể có ít lý do để thay đổi trong trường hợp đó, vì thực tế có thể dễ dàng ổn định biểu diễn dữ liệu hơn giao diện trừu tượng. Bộ xử lý video có thể giải quyết được ý tưởng sử dụng định dạng pixel RGBA 32 bit và quyết định thiết kế đó có thể không thay đổi trong nhiều năm tới.
Lý tưởng nhất là bạn muốn các phụ thuộc chảy vào sự ổn định (những thứ không thay đổi) bởi vì thay đổi một cái gì đó có nhiều phụ thuộc nhân với chi phí với số lượng phụ thuộc. Điều đó có thể hoặc không thể trừu tượng trong mọi trường hợp. Tất nhiên đó là bỏ qua những lợi ích của việc che giấu thông tin trong việc duy trì bất biến, nhưng từ quan điểm khớp nối, điểm chính của việc tách rời là làm cho mọi thứ ít tốn kém hơn để thay đổi. Điều đó có nghĩa là chuyển hướng phụ thuộc từ những thứ có thể thay đổi sang những thứ không thay đổi và điều đó không giúp ích chút nào nếu giao diện trừu tượng của bạn là phần thay đổi nhanh nhất trong cấu trúc dữ liệu của bạn.
Nếu bạn muốn ít nhất cải thiện điều đó một chút từ góc độ khớp nối, thì hãy tách các phần nút của bạn mà khách hàng cần truy cập khỏi các phần không có. Tôi giả sử khách hàng ít nhất không phải cập nhật các liên kết của nút, ví dụ, vì vậy không cần phải phơi bày các liên kết. Ít nhất bạn sẽ có thể đưa ra một số tổng hợp giá trị tách biệt với toàn bộ các nút đại diện cho khách hàng để truy cập / sửa đổi, như thế nào NodeValue
.