Lỗi bộ nhớ cache và khả năng sử dụng trong Hệ thống thực thể


18

Gần đây tôi đã nghiên cứu và triển khai Hệ thống thực thể cho khung của mình. Tôi nghĩ rằng tôi đã đọc hầu hết các bài báo, reddits và câu hỏi về nó mà tôi có thể tìm thấy, và cho đến nay tôi nghĩ rằng tôi đang nắm bắt ý tưởng đủ tốt.

Tuy nhiên, nó đặt ra một số câu hỏi về hành vi C ++ tổng thể, ngôn ngữ tôi triển khai hệ thống thực thể, cũng như một số vấn đề về khả năng sử dụng.

Vì vậy, một cách tiếp cận sẽ là lưu trữ trực tiếp một mảng các thành phần trong thực thể, điều mà tôi đã không làm vì nó phá hỏng vị trí bộ đệm khi lặp qua dữ liệu. Do đó, tôi quyết định có một mảng cho mỗi loại thành phần, vì vậy tất cả các thành phần cùng loại đều nằm liền kề trong bộ nhớ, đây sẽ là giải pháp tối ưu để lặp lại nhanh.

Nhưng, khi tôi lặp lại các mảng thành phần để làm một cái gì đó với chúng từ một hệ thống khi thực hiện chơi trò chơi thực tế, tôi nhận thấy rằng tôi hầu như luôn làm việc với hai hoặc nhiều loại thành phần cùng một lúc. Ví dụ, hệ thống kết xuất sử dụng thành phần Biến đổi và Mô hình để thực hiện cuộc gọi kết xuất. Câu hỏi của tôi là, vì tôi không lặp lại tuyến tính một mảng liền kề tại một thời điểm trong các trường hợp này, tôi có ngay lập tức hy sinh hiệu suất đạt được từ việc phân bổ các thành phần theo cách này không? Có phải là một vấn đề khi tôi lặp lại, trong C ++, hai mảng liền kề khác nhau và sử dụng dữ liệu từ cả hai ở mỗi chu kỳ?

Một điều khác mà tôi muốn hỏi là làm thế nào người ta nên giữ các tham chiếu đến các thành phần hoặc thực thể, vì bản chất của cách các thành phần được đặt trong bộ nhớ, chúng có thể dễ dàng chuyển đổi vị trí trong mảng hoặc mảng có thể được phân bổ lại để mở rộng hoặc thu hẹp, để lại con trỏ thành phần của tôi hoặc xử lý không hợp lệ. Bạn khuyên bạn nên xử lý những trường hợp này như thế nào, vì tôi thường thấy mình muốn vận hành các biến đổi và các thành phần khác ở mọi khung hình và nếu tay cầm hoặc con trỏ của tôi không hợp lệ, thì việc tìm kiếm mọi khung hình sẽ khá lộn xộn.


4
Tôi sẽ không bận tâm đưa các thành phần vào một bộ nhớ liên tục mà chỉ phân bổ bộ nhớ cho từng thành phần một cách linh hoạt. Bộ nhớ liền kề không có khả năng cung cấp cho bạn bất kỳ hiệu suất bộ nhớ cache nào vì bạn có khả năng truy cập các thành phần theo thứ tự khá ngẫu nhiên.
JarkkoL

@Grimshaw Dưới đây là một bài viết thú vị để đọc: harmful.cat-v.org/software/OO_programming/_pdf/...
Raxvan

@JarkkoL -10 điểm. Nó thực sự làm tổn thương hiệu năng nếu bạn xây dựng bộ đệm hệ thống thân thiện và truy cập nó theo cách ngẫu nhiên , nó chỉ ngu ngốc bởi âm thanh của nó. Điểm của nó để truy cập nó theo cách tuyến tính . Nghệ thuật của ECS và hiệu suất đạt được là về việc viết C / S được truy cập theo cách tuyến tính.
wonderra

@Grimshaw đừng quên bộ nhớ cache lớn hơn một số nguyên. Bạn đã có sẵn vài KB bộ đệm L1 (và MB khác), nếu bạn không làm điều gì quái gở, bạn có thể truy cập vài hệ thống cùng một lúc và thân thiện với bộ đệm.
wonderra

2
@wondra Làm thế nào bạn sẽ đảm bảo truy cập tuyến tính đến các thành phần? Giả sử nếu tôi thu thập các thành phần để kết xuất và muốn các thực thể được xử lý theo thứ tự giảm dần từ máy ảnh. Các thành phần kết xuất cho các thực thể này sẽ không được truy cập tuyến tính trong bộ nhớ. Mặc dù những gì bạn nói là điều tuyệt vời trong lý thuyết, tôi không thấy nó hoạt động trong thực tế, nhưng tôi rất vui nếu bạn chứng minh tôi sai (:
JarkkoL

Câu trả lời:


13

Đầu tiên, tôi sẽ không nói rằng trong trường hợp này, bạn đang tối ưu hóa quá sớm, tùy thuộc vào trường hợp sử dụng của bạn. Trong mọi trường hợp, bạn đã hỏi một câu hỏi thú vị và khi tôi có kinh nghiệm với điều này, tôi sẽ cân nhắc. Tôi sẽ cố gắng giải thích cách tôi kết thúc việc làm và những gì tôi tìm thấy trên đường.

  • Mỗi thực thể giữ một vectơ của các thẻ điều khiển thành phần chung có thể đại diện cho bất kỳ loại nào.
  • Mỗi tay cầm thành phần có thể được hủy đăng ký để tạo ra một con trỏ T * thô. *Xem bên dưới.
  • Mỗi loại thành phần có một nhóm riêng, một khối bộ nhớ liên tục (kích thước cố định trong trường hợp của tôi).

Cần lưu ý rằng không, bạn sẽ không thể luôn luôn đi qua một nhóm thành phần và làm điều lý tưởng, sạch sẽ. Như bạn đã nói, có những liên kết không thể vượt qua giữa các thành phần, trong đó bạn thực sự cần xử lý mọi thứ tại một thực thể tại một thời điểm.

Tuy nhiên, có những trường hợp (như tôi đã tìm thấy) trong đó, thực sự, bạn có thể viết một vòng lặp for cho một loại thành phần cụ thể và sử dụng rất tốt các dòng bộ đệm CPU của bạn. Đối với những người không biết hoặc muốn biết thêm, hãy xem https://en.wikipedia.org/wiki/Locality_of numference . Cùng một lưu ý, khi có thể, hãy cố gắng giữ kích thước thành phần của bạn nhỏ hơn hoặc bằng kích thước dòng bộ đệm CPU của bạn. Kích thước dòng của tôi là 64 byte, mà tôi tin là phổ biến.

Trong trường hợp của tôi, làm cho nỗ lực thực hiện hệ thống là hoàn toàn xứng đáng. Tôi thấy hiệu suất tăng rõ rệt (tất nhiên được mô tả). Bạn sẽ cần phải tự quyết định xem đó có phải là một ý tưởng tốt hay không. Những thành tựu lớn nhất trong hiệu suất tôi thấy ở hơn 1000 thực thể.

Một điều khác mà tôi muốn hỏi là làm thế nào người ta nên giữ các tham chiếu đến các thành phần hoặc thực thể, vì bản chất của cách các thành phần được đặt trong bộ nhớ, chúng có thể dễ dàng chuyển đổi vị trí trong mảng hoặc mảng có thể được phân bổ lại để mở rộng hoặc thu hẹp, để lại con trỏ thành phần của tôi hoặc xử lý không hợp lệ. Bạn khuyên bạn nên xử lý những trường hợp này như thế nào, vì tôi thường thấy mình muốn vận hành các biến đổi và các thành phần khác ở mọi khung hình và nếu tay cầm hoặc con trỏ của tôi không hợp lệ, thì việc tìm kiếm mọi khung hình sẽ khá lộn xộn.

Tôi cũng đã giải quyết vấn đề này cá nhân. Tôi đã kết thúc có một hệ thống trong đó:

  • Mỗi thành phần xử lý giữ một tham chiếu đến một chỉ mục nhóm
  • Khi một thành phần bị 'xóa' hoặc 'bị xóa' khỏi nhóm, thành phần cuối cùng trong nhóm đó sẽ được di chuyển (theo nghĩa đen với std :: move) đến vị trí hiện tại hoặc không có gì nếu bạn vừa xóa thành phần cuối cùng.
  • Khi xảy ra 'hoán đổi', tôi có một cuộc gọi lại thông báo cho bất kỳ người nghe nào, để họ có thể cập nhật bất kỳ con trỏ cụ thể nào (ví dụ T *).

* Tôi thấy rằng việc cố gắng luôn xử lý thành phần xử lý trong thời gian chạy trong các phần nhất định của mã sử dụng cao với số lượng thực thể mà tôi đang xử lý là một vấn đề về hiệu năng. Do đó, bây giờ tôi duy trì một số con trỏ T thô trong các phần quan trọng về hiệu năng của dự án, nhưng nếu không thì tôi sử dụng các tay cầm thành phần chung, nên được sử dụng khi có thể. Tôi giữ chúng hợp lệ như đã đề cập ở trên, với hệ thống gọi lại. Bạn có thể không cần phải đi xa như vậy.

Trên hết, chỉ cần thử mọi thứ. Cho đến khi bạn có được một kịch bản trong thế giới thực, bất cứ ai nói ở đây chỉ là một cách làm, điều này có thể không phù hợp với bạn.

cái đó có giúp ích không? Tôi sẽ cố gắng làm rõ bất cứ điều gì không rõ ràng. Ngoài ra bất kỳ sửa chữa được đánh giá cao.


Được đánh giá cao, đây là một câu trả lời thực sự tốt, và trong khi nó có thể không phải là một viên đạn bạc, thật tốt khi thấy ai đó có ý tưởng thiết kế tương tự. Tôi cũng có một số thủ thuật của bạn được thực hiện trong ES của tôi và chúng có vẻ thực tế. Cảm ơn rất nhiều! Hãy bình luận ý kiến ​​thêm nếu họ đưa ra.
Grimshaw

5

Để trả lời điều này:

Câu hỏi của tôi là, vì tôi không lặp lại tuyến tính một mảng liền kề tại một thời điểm trong các trường hợp này, tôi có ngay lập tức hy sinh hiệu suất đạt được từ việc phân bổ các thành phần theo cách này không? Có phải là một vấn đề khi tôi lặp lại, trong C ++, hai mảng liền kề khác nhau và sử dụng dữ liệu từ cả hai ở mỗi chu kỳ?

Không (ít nhất là không nhất thiết). Trong hầu hết các trường hợp, bộ điều khiển bộ đệm có thể xử lý việc đọc từ nhiều hơn một mảng liền kề một cách hiệu quả. Phần quan trọng là thử nơi có thể truy cập từng mảng một cách tuyến tính.

Để chứng minh điều này, tôi đã viết một điểm chuẩn nhỏ (áp dụng các thông số chuẩn thông thường).

Bắt đầu với một cấu trúc vector đơn giản:

struct float3 { float x, y, z; };

Tôi thấy rằng một vòng lặp tổng hợp từng phần tử của hai mảng riêng biệt và lưu trữ kết quả trong một phần ba thực hiện chính xác giống như một phiên bản trong đó dữ liệu nguồn được xen kẽ trong một mảng và kết quả được lưu trữ trong một phần ba. Tuy nhiên, tôi đã tìm thấy, nếu tôi xen kẽ kết quả với nguồn, hiệu suất phải chịu (khoảng 2 nhân tố).

Nếu tôi truy cập dữ liệu ngẫu nhiên, hiệu suất bị ảnh hưởng bởi hệ số từ 10 đến 20.

Thời gian (10.000.000 yếu tố)

truy cập tuyến tính

  • mảng riêng 0,21s
  • nguồn xen kẽ 0,21s
  • nguồn xen kẽ và kết quả 0,48s

truy cập ngẫu nhiên (uncomment Random_shuffle)

  • mảng riêng 2,42s
  • nguồn xen kẽ 4,43s
  • nguồn xen kẽ và kết quả 4,00

Nguồn (được biên dịch với Visual Studio 2013):

#include <Windows.h>
#include <vector>
#include <algorithm>
#include <iostream>

struct float3 { float x, y, z; };

float3 operator+( float3 const &a, float3 const &b )
{
    return float3{ a.x + b.x, a.y + b.y, a.z + b.z };
}

struct Both { float3 a, b; };

struct All { float3 a, b, res; };


// A version without any indirection
void sum( float3 *a, float3 *b, float3 *res, int n )
{
    for( int i = 0; i < n; ++i )
        *res++ = *a++ + *b++;
}

void sum( float3 *a, float3 *b, float3 *res, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        res[*index] = a[*index] + b[*index];
}

void sum( Both *both, float3 *res, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        res[*index] = both[*index].a + both[*index].b;
}

void sum( All *all, int *index, int n )
{
    for( int i = 0; i < n; ++i, ++index )
        all[*index].res = all[*index].a + all[*index].b;
}

class PerformanceTimer
{
public:
    PerformanceTimer() { QueryPerformanceCounter( &start ); }
    double time()
    {
        LARGE_INTEGER now, freq;
        QueryPerformanceCounter( &now );
        QueryPerformanceFrequency( &freq );
        return double( now.QuadPart - start.QuadPart ) / double( freq.QuadPart );
    }
private:
    LARGE_INTEGER start;
};

int main( int argc, char* argv[] )
{
    const int count = 10000000;

    std::vector< float3 > a( count, float3{ 1.f, 2.f, 3.f } );
    std::vector< float3 > b( count, float3{ 1.f, 2.f, 3.f } );
    std::vector< float3 > res( count );

    std::vector< All > all( count, All{ { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f } } );
    std::vector< Both > both( count, Both{ { 1.f, 2.f, 3.f }, { 1.f, 2.f, 3.f } } );

    std::vector< int > index( count );
    int n = 0;
    std::generate( index.begin(), index.end(), [&]{ return n++; } );
    //std::random_shuffle( index.begin(), index.end() );

    PerformanceTimer timer;
    // uncomment version to test
    //sum( &a[0], &b[0], &res[0], &index[0], count );
    //sum( &both[0], &res[0], &index[0], count );
    //sum( &all[0], &index[0], count );
    std::cout << timer.time();
    return 0;
}

1
Điều này giúp ích rất nhiều cho sự nghi ngờ của tôi về địa phương bộ đệm, cảm ơn!
Grimshaw

Câu trả lời đơn giản nhưng thú vị mà tôi cũng thấy yên tâm :) Tôi rất muốn xem các kết quả này khác nhau như thế nào đối với số lượng vật phẩm khác nhau (ví dụ: 1000 thay vì 10.000.000?) Hoặc nếu bạn có nhiều mảng giá trị hơn (ví dụ: tổng các yếu tố của 3 -5 mảng riêng biệt và lưu trữ giá trị vào một mảng riêng khác).
Awesomania

2

Trả lời ngắn: Hồ sơ sau đó tối ưu hóa.

Câu trả lời dài:

Nhưng, khi tôi lặp lại các mảng thành phần để làm một cái gì đó với chúng từ một hệ thống khi thực hiện chơi trò chơi thực tế, tôi nhận thấy rằng tôi hầu như luôn làm việc với hai hoặc nhiều loại thành phần cùng một lúc.

Có phải là một vấn đề khi tôi lặp lại, trong C ++, hai mảng liền kề khác nhau và sử dụng dữ liệu từ cả hai ở mỗi chu kỳ?

C ++ không chịu trách nhiệm cho các lỗi nhớ cache, vì nó áp dụng cho bất kỳ ngôn ngữ lập trình nào. Điều này có liên quan đến cách thức hoạt động của kiến ​​trúc CPU hiện đại.

Vấn đề của bạn có thể là một ví dụ tốt về những gì có thể được gọi là tối ưu hóa trước khi trưởng thành .

Theo tôi, bạn đã tối ưu hóa quá sớm cho địa phương bộ đệm mà không cần nhìn vào các mẫu truy cập bộ nhớ chương trình. Nhưng câu hỏi lớn hơn là bạn có thực sự cần loại tối ưu hóa này (địa phương tham khảo) không?

Sương mù của Agner gợi ý rằng bạn không nên tối ưu hóa trước khi lập hồ sơ cho ứng dụng của mình và / hoặc biết chắc chắn nơi tắc nghẽn. (Đây là tất cả được đề cập trong hướng dẫn tuyệt vời của mình. Liên kết dưới đây)

Sẽ rất hữu ích khi biết cách tổ chức bộ đệm nếu bạn đang tạo các chương trình có cấu trúc dữ liệu lớn với quyền truy cập không tuần tự và bạn muốn ngăn chặn sự tranh chấp bộ đệm. Bạn có thể bỏ qua phần này nếu bạn hài lòng với các hướng dẫn heuristic hơn.

Thật không may, những gì bạn đã làm thực sự cho rằng việc phân bổ một loại thành phần cho mỗi mảng sẽ mang lại cho bạn hiệu suất tốt hơn, trong khi thực tế bạn có thể đã gây ra nhiều lỗi nhớ cache hơn hoặc thậm chí là tranh chấp bộ đệm.

Bạn chắc chắn nên xem hướng dẫn tối ưu hóa C ++ tuyệt vời của anh ấy .

Một điều khác mà tôi muốn hỏi, là làm thế nào người ta nên giữ các tham chiếu đến các thành phần hoặc thực thể, vì bản chất của cách các thành phần được đặt trong bộ nhớ.

Cá nhân tôi sẽ phân bổ hầu hết các thành phần được sử dụng cùng nhau trong một khối bộ nhớ duy nhất để chúng có địa chỉ "gần". Ví dụ, một mảng sẽ trông như thế:

[{ID0 Transform Model PhysicsComp }{ID10 Transform Model PhysicsComp }{ID2 Transform Model PhysicsComp }..] và sau đó bắt đầu tối ưu hóa từ đó nếu hiệu suất không "đủ tốt".


Câu hỏi của tôi là về ý nghĩa mà kiến ​​trúc của tôi có thể có đối với hiệu suất, vấn đề không phải là tối ưu hóa mà là chọn cách tổ chức mọi thứ bên trong. Bất kể cách nào nó đang diễn ra bên trong, tôi muốn mã trò chơi của mình tương tác với nó theo cách đồng nhất trong trường hợp tôi muốn thay đổi sau này. Câu trả lời của bạn là tốt ngay cả khi nó có thể cung cấp các đề xuất bổ sung về cách lưu trữ dữ liệu. Nâng cao.
Grimshaw

Từ những gì tôi thấy, có ba cách chính để lưu trữ các thành phần, tất cả được ghép trong một mảng duy nhất cho mỗi thực thể, tất cả được ghép với nhau theo kiểu trong các mảng riêng lẻ và nếu tôi hiểu chính xác, bạn đề nghị lưu trữ các Thực thể khác nhau liên tục trong một mảng lớn, và mỗi thực thể, có tất cả các thành phần của nó với nhau?
Grimshaw

@Grimshaw Như tôi đã đề cập trong câu trả lời, kiến ​​trúc của bạn không được đảm bảo để cho kết quả tốt hơn so với mẫu phân bổ thông thường. Vì bạn không thực sự biết mẫu truy cập của các ứng dụng của mình. Tối ưu hóa như vậy thường được thực hiện sau một số nghiên cứu / bằng chứng. Về đề xuất của tôi, lưu trữ các thành phần liên quan với nhau trong cùng một bộ nhớ và các thành phần khác ở các vị trí khác nhau. Đây là một nền tảng giữa tất cả hoặc không có gì. Tuy nhiên, tôi vẫn cho rằng thật khó để dự đoán kiến ​​trúc của bạn sẽ ảnh hưởng đến kết quả như thế nào khi có bao nhiêu điều kiện diễn ra.
Concept3d

Các downvoter quan tâm để giải thích? Chỉ cần chỉ ra vấn đề trong câu trả lời của tôi. Tốt hơn là đưa ra một câu trả lời tốt hơn.
Concept3d

1

Câu hỏi của tôi là, vì tôi không lặp lại tuyến tính một mảng liền kề tại một thời điểm trong các trường hợp này, tôi có ngay lập tức hy sinh hiệu suất đạt được từ việc phân bổ các thành phần theo cách này không?

Rất có thể là bạn sẽ nhận được ít bộ nhớ cache hơn với các mảng "dọc" riêng biệt cho mỗi loại thành phần hơn là xen kẽ các thành phần được gắn vào một thực thể trong một khối có kích thước thay đổi "theo chiều ngang".

Lý do là bởi vì, đầu tiên, biểu diễn "dọc" sẽ có xu hướng sử dụng ít bộ nhớ hơn. Bạn không phải lo lắng về việc căn chỉnh cho các mảng đồng nhất được phân bổ liên tục. Với các loại không đồng nhất được phân bổ vào nhóm bộ nhớ, bạn không phải lo lắng về việc căn chỉnh vì phần tử đầu tiên trong mảng có thể có các yêu cầu căn chỉnh và kích thước hoàn toàn khác với phần thứ hai. Kết quả là bạn sẽ thường cần thêm phần đệm, ví dụ như một ví dụ đơn giản:

// Assuming 8-bit chars and 64-bit doubles.
struct Foo
{
    // 1 byte
    char a;

    // 1 byte
    char b;
};

struct Bar
{
    // 8 bytes
    double opacity;

    // 8 bytes
    double radius;
};

Hãy nói rằng chúng ta muốn interleave FooBarvà lưu trữ chúng ngay bên cạnh nhau trong bộ nhớ:

// Assuming 8-bit chars and 64-bit doubles.
struct FooBar
{
    // 1 byte
    char a;

    // 1 byte
    char b;

    // 6 bytes padding for 64-bit alignment of 'opacity'

    // 8 bytes
    double opacity;

    // 8 bytes
    double radius;
};

Bây giờ thay vì lấy 18 byte để lưu trữ Foo và Bar trong các vùng bộ nhớ riêng biệt, phải mất 24 byte để kết hợp chúng. Không thành vấn đề nếu bạn trao đổi thứ tự:

// Assuming 8-bit chars and 64-bit doubles.
struct BarFoo
{
    // 8 bytes
    double opacity;

    // 8 bytes
    double radius;

    // 1 byte
    char a;

    // 1 byte
    char b;

    // 6 bytes padding for 64-bit alignment of 'opacity'
};

Nếu bạn chiếm nhiều bộ nhớ hơn trong ngữ cảnh truy cập tuần tự mà không cải thiện đáng kể các mẫu truy cập, thì nhìn chung bạn sẽ phải chịu nhiều lỗi nhớ cache hơn. Trên hết, bước tiến để có được từ một thực thể này đến lần tăng tiếp theo và đến một kích thước thay đổi, khiến bạn phải thực hiện các bước nhảy có kích thước thay đổi trong bộ nhớ để chuyển từ thực thể này sang thực thể tiếp theo để xem những thực thể nào có các thành phần bạn ' lại quan tâm

Vì vậy, sử dụng biểu diễn "dọc" khi bạn lưu trữ các loại thành phần thực sự có khả năng tối ưu hơn so với các lựa chọn thay thế "ngang". Điều đó nói rằng, vấn đề với lỗi nhớ cache với biểu diễn dọc có thể được minh họa ở đây:

nhập mô tả hình ảnh ở đây

Trong đó các mũi tên chỉ đơn giản chỉ ra rằng thực thể "sở hữu" một thành phần. Chúng ta có thể thấy rằng nếu chúng ta cố gắng truy cập tất cả các thành phần chuyển động và kết xuất của các thực thể có cả hai, cuối cùng chúng ta sẽ nhảy khắp nơi trong bộ nhớ. Kiểu truy cập lẻ tẻ đó có thể khiến bạn tải dữ liệu vào một dòng bộ đệm để truy cập, giả sử, một thành phần chuyển động, sau đó truy cập vào nhiều thành phần hơn và dữ liệu trước đó đã bị trục xuất, chỉ để tải lại cùng một vùng bộ nhớ đã bị đuổi ra khỏi một chuyển động khác thành phần. Vì vậy, điều đó có thể rất lãng phí khi tải cùng một vùng bộ nhớ chính xác nhiều lần vào một dòng bộ đệm chỉ để lặp qua và truy cập danh sách các thành phần.

Hãy dọn dẹp mớ hỗn độn đó một chút để chúng ta có thể nhìn rõ hơn:

nhập mô tả hình ảnh ở đây

Lưu ý rằng nếu bạn gặp phải loại kịch bản này, thường thì rất lâu sau khi trò chơi bắt đầu chạy, sau khi nhiều thành phần và thực thể đã được thêm và xóa. Nói chung khi trò chơi bắt đầu, bạn có thể thêm tất cả các thực thể và các thành phần có liên quan lại với nhau, tại thời điểm đó chúng có thể có một mẫu truy cập tuần tự, rất trật tự với địa phương không gian tốt. Sau rất nhiều lần gỡ bỏ và chèn thêm, cuối cùng bạn có thể nhận được một cái gì đó giống như mớ hỗn độn ở trên.

Một cách rất dễ dàng để cải thiện tình huống đó là chỉ đơn giản là sắp xếp các thành phần của bạn dựa trên ID / chỉ mục thực thể sở hữu chúng. Tại thời điểm đó, bạn nhận được một cái gì đó như thế này:

nhập mô tả hình ảnh ở đây

Và đó là một mẫu truy cập thân thiện với bộ nhớ cache hơn nhiều. Nó không hoàn hảo vì chúng ta có thể thấy rằng chúng ta phải bỏ qua một số thành phần kết xuất và chuyển động ở đây và ở đó vì hệ thống của chúng ta chỉ quan tâm đến các thực thể có cả hai và một số thực thể chỉ có thành phần chuyển động và một số chỉ có thành phần kết xuất , nhưng ít nhất bạn cuối cùng cũng có thể xử lý một số thành phần tiếp giáp (thông thường hơn, thông thường, vì thông thường, bạn sẽ đính kèm các thành phần quan tâm có liên quan, như có thể nhiều thực thể trong hệ thống của bạn có thành phần chuyển động sẽ có thành phần kết xuất hơn không phải).

Quan trọng nhất, một khi bạn đã sắp xếp những thứ này, bạn sẽ không tải dữ liệu một vùng bộ nhớ vào một dòng bộ đệm để sau đó tải lại nó trong một vòng lặp.

Và điều này không đòi hỏi một số thiết kế cực kỳ phức tạp, chỉ là một loại cơ số thời gian tuyến tính vượt qua mọi lúc, sau đó, có thể sau khi bạn đã chèn và loại bỏ một loạt các thành phần cho một loại thành phần cụ thể, tại đó bạn có thể đánh dấu nó là cần được sắp xếp Một loại cơ số được triển khai hợp lý (thậm chí bạn có thể song song hóa nó, mà tôi làm) có thể sắp xếp một triệu phần tử trong khoảng 6ms trên i7 lõi ​​tứ của tôi, như được minh họa ở đây:

Sorting 1000000 elements 32 times...
mt_sort_int: {0.203000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_sort: {1.248000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
mt_radix_sort: {0.202000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
std::sort: {1.810000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]
qsort: {2.777000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]

Ở trên là sắp xếp một triệu phần tử 32 lần (bao gồm cả thời gian để memcpykết quả trước và sau khi sắp xếp). Và tôi cho rằng hầu hết thời gian bạn sẽ không thực sự có hàng triệu thành phần để sắp xếp, vì vậy bạn sẽ rất dễ dàng có thể lén điều này ngay bây giờ và ở đó mà không gây ra bất kỳ sự chậm trễ đáng chú ý nào.

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.