การพลาดแคชและการใช้งานใน Entity Systems


18

เมื่อเร็ว ๆ นี้ฉันได้ทำการค้นคว้าและนำระบบ Entity มาใช้สำหรับกรอบงานของฉัน ฉันคิดว่าฉันอ่านบทความเกือบทั้งหมด reddits และคำถามเกี่ยวกับมันที่ฉันสามารถหาได้และจนถึงตอนนี้ฉันคิดว่าฉันเข้าใจความคิดที่ดีพอ

อย่างไรก็ตามมันทำให้เกิดคำถามบางอย่างเกี่ยวกับพฤติกรรม C ++ โดยรวมภาษาที่ฉันใช้ระบบเอนทิตีในรวมถึงปัญหาการใช้งานบางอย่าง

ดังนั้นวิธีการหนึ่งก็คือการจัดเก็บอาร์เรย์ของส่วนประกอบในเอนทิตีโดยตรงซึ่งฉันไม่ได้ทำเพราะมันทำลายสถานที่เก็บแคชเมื่อวนซ้ำผ่านข้อมูล ด้วยเหตุนี้ฉันจึงตัดสินใจที่จะมีหนึ่งอาเรย์ต่อประเภทส่วนประกอบดังนั้นส่วนประกอบทั้งหมดของประเภทเดียวกันจึงต่อเนื่องกันในหน่วยความจำซึ่งควรเป็นทางออกที่ดีที่สุดสำหรับการวนซ้ำอย่างรวดเร็ว

แต่เมื่อฉันต้องทำซ้ำอาร์เรย์ของส่วนประกอบเพื่อทำบางสิ่งบางอย่างกับพวกเขาจากระบบในการติดตั้งเกมจริงฉันสังเกตว่าฉันมักจะทำงานกับส่วนประกอบสองชนิดหรือมากกว่าในคราวเดียว ตัวอย่างเช่นระบบการเรนเดอร์ใช้องค์ประกอบการแปลงและโมเดลร่วมกันเพื่อทำการเรียกการเรนเดอร์ คำถามของฉันคือเนื่องจากฉันไม่ได้วนซ้ำแถวลำดับที่ต่อเนื่องกันในแต่ละครั้งในกรณีเหล่านี้ฉันจะเสียสละประสิทธิภาพที่เพิ่มขึ้นทันทีจากการจัดสรรส่วนประกอบด้วยวิธีนี้หรือไม่? เป็นปัญหาหรือไม่เมื่อฉันทำซ้ำใน C ++ อาร์เรย์ที่ต่อเนื่องกันสองชุดและใช้ข้อมูลจากทั้งสองในแต่ละรอบ

อีกสิ่งหนึ่งที่ฉันต้องการถามคือควรเก็บการอ้างอิงถึงส่วนประกอบหรือเอนทิตีอย่างไรเนื่องจากลักษณะของการจัดวางองค์ประกอบในหน่วยความจำพวกเขาสามารถสลับตำแหน่งในอาเรย์ได้อย่างง่ายดาย ลดขนาดปล่อยให้ตัวชี้ส่วนประกอบของฉันหรือจัดการไม่ถูกต้อง คุณจะแนะนำให้จัดการกับกรณีเหล่านี้อย่างไรเนื่องจากฉันมักจะพบว่าตัวเองต้องการทำงานกับการแปลงและส่วนประกอบอื่น ๆ ทุกเฟรมและถ้ามือจับหรือตัวชี้ของฉันไม่ถูกต้องมันค่อนข้างยุ่งที่จะทำการค้นหาทุกเฟรม


4
ฉันจะไม่รบกวนการใส่ส่วนประกอบลงในหน่วยความจำต่อเนื่อง แต่เพียงจัดสรรหน่วยความจำสำหรับแต่ละองค์ประกอบแบบไดนามิก หน่วยความจำต่อเนื่องไม่น่าจะช่วยให้คุณได้รับประสิทธิภาพแคชเพราะคุณมีแนวโน้มที่จะเข้าถึงส่วนประกอบตามลำดับแบบสุ่มสวยอยู่แล้ว
JarkkoL

@Grimshaw นี่คือบทความที่น่าสนใจในการอ่าน: harmful.cat-v.org/software/OO_programming/_pdf/...
Raxvan

@ JarkkoL -10 คะแนน มันเจ็บประสิทธิภาพถ้าคุณสร้างแคชของระบบที่เป็นมิตรและเข้าถึงแบบสุ่มมันจะโง่โดยเสียงของมัน จุดของมันเข้าถึงได้ในวิธีการเชิงเส้น ศิลปะของ ECS และการเพิ่มประสิทธิภาพนั้นเกี่ยวกับการเขียน C / S ที่เข้าถึงในลักษณะเชิงเส้น
wondra

@Grimshaw อย่าลืมแคชใหญ่กว่าจำนวนเต็มหนึ่งตัว คุณมีแคช L1 หลาย KB (และเมกะไบต์อื่น ๆ ) หากคุณไม่ทำสิ่งใดที่ร้ายกาจคุณควรตกลงที่จะเข้าถึงระบบไม่กี่ระบบในครั้งเดียวและในขณะที่ใช้งานแคชได้ง่าย
wondra

2
@wondra คุณจะมั่นใจได้อย่างไรว่าการเข้าถึงส่วนประกอบเป็น Linear? ถ้าฉันรวบรวมส่วนประกอบสำหรับการเรนเดอร์และต้องการเอนทิตีที่ประมวลผลตามลำดับจากมากไปน้อยจากกล้อง องค์ประกอบการแสดงผลสำหรับเอนทิตีเหล่านี้จะไม่สามารถเข้าถึงเชิงเส้นในหน่วยความจำ ในขณะที่สิ่งที่คุณพูดเป็นสิ่งที่ดีในทางทฤษฎีฉันไม่เห็นมันทำงานในทางปฏิบัติ แต่ฉันดีใจที่คุณพิสูจน์ฉันผิด (:
JarkkoL

คำตอบ:


13

ครั้งแรกฉันจะไม่พูดว่าในกรณีนี้คุณกำลังปรับให้เร็วเกินไปขึ้นอยู่กับกรณีการใช้งานของคุณ ไม่ว่าในกรณีใดก็ตามคุณได้ถามคำถามที่น่าสนใจและเนื่องจากฉันมีประสบการณ์กับตัวเองฉันจะชั่งน้ำหนักฉันจะพยายามอธิบายว่าฉันลงเอยด้วยการทำสิ่งต่าง ๆ และสิ่งที่ฉันพบในระหว่างทาง

  • แต่ละเอนทิตีมีเวกเตอร์ของการจัดการส่วนประกอบทั่วไปซึ่งสามารถแสดงประเภทใดก็ได้
  • การจัดการส่วนประกอบแต่ละตัวสามารถกำหนดค่าใหม่เพื่อให้ได้ตัวชี้ T * แบบดิบ * ดูด้านล่าง
  • องค์ประกอบแต่ละประเภทมีกลุ่มของตัวเองบล็อกหน่วยความจำอย่างต่อเนื่อง (ขนาดคงที่ในกรณีของฉัน)

ควรสังเกตว่าไม่คุณจะไม่สามารถสำรวจกลุ่มส่วนประกอบและทำสิ่งที่สมบูรณ์แบบและสะอาด มีอย่างที่คุณพูดถึงลิงก์ที่หลีกเลี่ยงไม่ได้ระหว่างส่วนประกอบซึ่งคุณจำเป็นต้องประมวลผลสิ่งต่าง ๆ ในแต่ละครั้ง

อย่างไรก็ตามมีบางกรณี (ตามที่ฉันค้นพบ) ซึ่งแน่นอนคุณสามารถเขียนลูป for สำหรับประเภทคอมโพเนนต์เฉพาะและใช้ประโยชน์จากแคชแคชของคุณได้อย่างดีเยี่ยม สำหรับผู้ที่ไม่รู้จักหรือต้องการที่จะทราบข้อมูลเพิ่มเติมดูที่https://en.wikipedia.org/wiki/Locality_of_reference ในบันทึกย่อเดียวกันถ้าเป็นไปได้พยายามรักษาขนาดส่วนประกอบของคุณให้น้อยกว่าหรือเท่ากับขนาดสายแคช CPU ของคุณ ขนาดสายของฉันคือ 64 ไบต์ซึ่งฉันเชื่อว่าเป็นเรื่องธรรมดา

ในกรณีของฉันการใช้ความพยายามในการนำระบบมาใช้นั้นคุ้มค่า ฉันเห็นประสิทธิภาพที่มองเห็นได้ชัดเจน (ประวัติแน่นอน) คุณจะต้องตัดสินใจด้วยตัวเองว่าเป็นความคิดที่ดีหรือไม่ กำไรที่เพิ่มขึ้นมากที่สุดที่ฉันเห็นจากหน่วยงานกว่า 1,000 รายการ

อีกสิ่งหนึ่งที่ฉันต้องการถามคือควรเก็บการอ้างอิงถึงส่วนประกอบหรือเอนทิตีอย่างไรเนื่องจากลักษณะของการจัดวางองค์ประกอบในหน่วยความจำพวกเขาสามารถสลับตำแหน่งในอาเรย์ได้อย่างง่ายดาย ลดขนาดปล่อยให้ตัวชี้ส่วนประกอบของฉันหรือจัดการไม่ถูกต้อง คุณจะแนะนำให้จัดการกับกรณีเหล่านี้อย่างไรเนื่องจากฉันมักจะพบว่าตัวเองต้องการทำงานกับการแปลงและส่วนประกอบอื่น ๆ ทุกเฟรมและถ้ามือจับหรือตัวชี้ของฉันไม่ถูกต้องมันค่อนข้างยุ่งที่จะทำการค้นหาทุกเฟรม

ฉันยังแก้ไขปัญหานี้เป็นการส่วนตัวด้วย ฉันสิ้นสุดมีระบบที่:

  • แต่ละหมายเลขอ้างอิงคอมโพเนนต์เก็บการอ้างอิงไปยังดัชนีพู
  • เมื่อคอมโพเนนต์ถูก 'ลบ' หรือ 'ลบ' ออกจากพูลส่วนประกอบสุดท้ายภายในพูลนั้นจะถูกย้าย (ตามตัวอักษรด้วย std :: move) ไปยังตำแหน่งว่างในขณะนี้หรือไม่มีเลยถ้าคุณเพิ่งลบองค์ประกอบสุดท้าย
  • เมื่อมีการ 'สลับ' ฉันมีการติดต่อกลับที่แจ้งผู้ฟังเพื่อให้พวกเขาสามารถอัปเดตพอยน์เตอร์ที่เป็นรูปธรรม (เช่น T *)

* ฉันพบว่าการพยายามที่จะจัดการกับองค์ประกอบที่อ้างถึงในรันไทม์ในบางส่วนของรหัสการใช้งานสูงที่มีจำนวนเอนทิตีที่ฉันจัดการอยู่นั้นเป็นปัญหาด้านประสิทธิภาพ ด้วยเหตุนี้ฉันจึงรักษาพอยน์เตอร์พอยน์เตอร์บางส่วนในส่วนที่มีความสำคัญต่อประสิทธิภาพของโครงการของฉัน แต่ไม่เช่นนั้นฉันก็ใช้ตัวจัดการคอมโพเนนต์ทั่วไป ฉันให้พวกเขาถูกต้องตามที่กล่าวไว้ข้างต้นด้วยระบบโทรกลับ คุณอาจไม่จำเป็นต้องไปให้ไกล

เหนือสิ่งอื่นใดเพียงลองทำ จนกว่าคุณจะได้รับสถานการณ์ในโลกแห่งความเป็นจริงสิ่งที่ทุกคนพูดถึงที่นี่เป็นเพียงวิธีหนึ่งในการทำสิ่งต่าง ๆ ซึ่งอาจไม่เหมาะสมสำหรับคุณ

มันช่วยได้ไหม ฉันจะพยายามอธิบายสิ่งที่ไม่ชัดเจน ยังมีการแก้ไขใด ๆ ที่ชื่นชม


โหวตขึ้นนี่เป็นคำตอบที่ดีจริงๆและแม้ว่ามันอาจจะไม่ใช่กระสุนเงิน แต่ก็ยังดีที่เห็นคนที่มีแนวคิดการออกแบบที่คล้ายกัน ฉันมีเทคนิคบางอย่างที่คุณนำมาใช้ใน ES ของฉันเช่นกันและพวกเขาก็ดูเหมือนจะใช้งานได้จริง ขอบคุณมาก! รู้สึกอิสระที่จะแสดงความคิดเห็นแนวคิดเพิ่มเติมหากพวกเขาขึ้นมา
Grimshaw

5

เพื่อตอบเพียงแค่นี้:

คำถามของฉันคือเนื่องจากฉันไม่ได้วนซ้ำแถวลำดับที่ต่อเนื่องกันในแต่ละครั้งในกรณีเหล่านี้ฉันจะเสียสละประสิทธิภาพที่เพิ่มขึ้นทันทีจากการจัดสรรส่วนประกอบด้วยวิธีนี้หรือไม่? เป็นปัญหาหรือไม่เมื่อฉันทำซ้ำใน C ++ อาร์เรย์ที่ต่อเนื่องกันสองชุดและใช้ข้อมูลจากทั้งสองในแต่ละรอบ

ไม่ (อย่างน้อยก็ไม่จำเป็น) ในกรณีส่วนใหญ่แคชคอนโทรลเลอร์สามารถจัดการกับการอ่านจากอาร์เรย์ที่ต่อเนื่องกันได้มากกว่าหนึ่งชุดได้อย่างมีประสิทธิภาพ ส่วนที่สำคัญคือการพยายามเข้าถึงอาเรย์แต่ละรายการอย่างเป็นเส้นตรง

เพื่อแสดงสิ่งนี้ฉันได้เขียนเกณฑ์มาตรฐานขนาดเล็ก (ใช้เกณฑ์มาตรฐานทั่วไป)

เริ่มต้นด้วยโครงสร้างเวกเตอร์อย่างง่าย:

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

ฉันพบว่าการวนซ้ำการรวมองค์ประกอบของสองอาร์เรย์ที่แยกกันและการจัดเก็บผลลัพธ์ในลำดับที่สามดำเนินการเหมือนกับรุ่นที่ข้อมูลต้นฉบับถูกอินเตอร์ลีฟในอาร์เรย์เดียวและผลลัพธ์ที่เก็บไว้ในลำดับที่สาม อย่างไรก็ตามฉันพบว่าถ้าฉันแทรกผลลัพธ์กับแหล่งที่มาประสิทธิภาพก็ลดลง (ประมาณ 2 เท่า)

ถ้าฉันเข้าถึงข้อมูลแบบสุ่มประสิทธิภาพการทำงานที่ได้รับจากปัจจัยระหว่าง 10 ถึง 20

การจับเวลา (10,000,000 องค์ประกอบ)

การเข้าถึงเชิงเส้น

  • อาร์เรย์แยกต่างหาก 0.21 วินาที
  • 0.21s interleaved แหล่ง
  • interleaved source และผลลัพธ์ 0.48s

การเข้าถึงแบบสุ่ม (ไม่ใส่ข้อคิดเห็นสุ่ม_shuffle)

  • อาร์เรย์ที่แยกต่างหาก 2.42 วินาที
  • แหล่ง interleaved 4.43s
  • interleaved source และผลลัพธ์ 4.00s

แหล่งที่มา (รวบรวมด้วย 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
สิ่งนี้ช่วยได้มากกับข้อสงสัยเกี่ยวกับสถานที่ในแคชขอบคุณ!
Grimshaw

คำตอบที่เรียบง่าย แต่น่าสนใจที่ฉันก็มั่นใจเช่นกัน :) ฉันสนใจที่จะดูว่าผลลัพธ์เหล่านี้แตกต่างกันอย่างไรสำหรับการนับจำนวนรายการที่แตกต่างกัน (เช่น 1,000 แทน 10,000,000?) หรือหากคุณมีอาร์เรย์ของค่ามากขึ้น -5 อาร์เรย์ที่แยกต่างหากและเก็บค่าไว้ในอาร์เรย์อื่นที่แยกต่างหาก)
Awesomania

2

คำตอบสั้น ๆ : โปรไฟล์แล้วปรับให้เหมาะสม

คำตอบยาว:

แต่เมื่อฉันต้องทำซ้ำอาร์เรย์ของส่วนประกอบเพื่อทำอะไรบางอย่างกับพวกเขาจากระบบในการติดตั้งเกมจริงฉันสังเกตว่าฉันมักจะทำงานกับส่วนประกอบสองชนิดหรือมากกว่าในคราวเดียว

เป็นปัญหาหรือไม่เมื่อฉันทำซ้ำใน C ++ อาร์เรย์ที่ต่อเนื่องกันสองชุดและใช้ข้อมูลจากทั้งสองในแต่ละรอบ

C ++ ไม่รับผิดชอบต่อการสูญหายของแคชเนื่องจากใช้กับภาษาการเขียนโปรแกรมใด ๆ สิ่งนี้เกี่ยวข้องกับสถาปัตยกรรมของ CPU ที่ทันสมัย

ปัญหาของคุณอาจเป็นตัวอย่างที่ดีของสิ่งที่อาจเรียกว่าการปรับให้เหมาะสมก่อนวัยอันควรควร

ในความคิดของฉันคุณปรับให้เร็วเกินไปสำหรับตำแหน่งแคชโดยไม่ได้ดูรูปแบบการเข้าถึงหน่วยความจำของโปรแกรม แต่คำถามที่ใหญ่กว่าคือคุณต้องการการเพิ่มประสิทธิภาพประเภทนี้ (สถานที่อ้างอิง) หรือไม่

Fog ของ Agner แนะนำว่าคุณไม่ควรปรับให้เหมาะสมก่อนที่จะโพรไฟล์แอปพลิเคชันของคุณและ / หรือรู้ว่าคอขวดอยู่ตรงไหน (นี่คือทั้งหมดที่กล่าวถึงในคู่มือที่ยอดเยี่ยมของเขาลิงค์ด้านล่าง)

มีประโยชน์ที่จะทราบวิธีจัดระเบียบแคชหากคุณกำลังสร้างโปรแกรมที่มีโครงสร้างข้อมูลขนาดใหญ่ที่มีการเข้าถึงที่ไม่ต่อเนื่องและคุณต้องการป้องกันการแย่งชิงแคช คุณสามารถข้ามส่วนนี้ได้หากคุณพอใจกับแนวทางการแก้ปัญหาเพิ่มเติม

น่าเสียดายที่คุณคิดว่าการจัดสรรองค์ประกอบหนึ่งประเภทต่ออาร์เรย์จะให้ประสิทธิภาพที่ดีขึ้น แต่ในความเป็นจริงคุณอาจทำให้แคชหายไปมากกว่าหรือแม้แต่การแย่งแคช

คุณควรดูที่ยอดเยี่ยมของเขาอย่างแน่นอน c ++ คู่มือการเพิ่มประสิทธิภาพ

อีกสิ่งหนึ่งที่ฉันต้องการถามคือควรเก็บการอ้างอิงถึงส่วนประกอบหรือเอนทิตีอย่างไรเนื่องจากลักษณะของการวางองค์ประกอบในหน่วยความจำ

โดยส่วนตัวแล้วฉันจะจัดสรรส่วนประกอบที่ใช้มากที่สุดด้วยกันในบล็อกหน่วยความจำเดียวดังนั้นพวกเขาจึงมีที่อยู่ "ใกล้" ตัวอย่างเช่นอาร์เรย์จะมีลักษณะเช่นนั้น:

[{ID0 Transform Model PhysicsComp }{ID10 Transform Model PhysicsComp }{ID2 Transform Model PhysicsComp }..] จากนั้นเริ่มเพิ่มประสิทธิภาพจากที่นั่นหากประสิทธิภาพไม่ดีพอ


คำถามของฉันเกี่ยวกับความหมายที่สถาปัตยกรรมของฉันอาจมีต่อประสิทธิภาพการทำงานจุดไม่ได้เพิ่มประสิทธิภาพ แต่เพื่อเลือกวิธีการจัดระเบียบสิ่งต่าง ๆ ภายใน ไม่ว่าจะเกิดอะไรขึ้นข้างในฉันต้องการให้โค้ดเกมโต้ตอบกับมันในลักษณะที่เป็นเนื้อเดียวกันในกรณีที่ฉันต้องการเปลี่ยนในภายหลัง คำตอบของคุณดีแม้ว่าจะสามารถให้คำแนะนำเพิ่มเติมเกี่ยวกับวิธีการจัดเก็บข้อมูล upvoted
Grimshaw

จากสิ่งที่ฉันเห็นมีสามวิธีหลักในการจัดเก็บส่วนประกอบทั้งหมดรวมกันในอาเรย์เดี่ยวต่อเอนทิตี้ทั้งหมดรวมเข้าด้วยกันตามประเภทในแต่ละอาร์เรย์และถ้าฉันเข้าใจถูกต้องคุณแนะนำให้เก็บเอนทิตี้ต่าง ๆ และต่อเอนทิตีองค์ประกอบทั้งหมดของมันอยู่ด้วยกันไหม?
Grimshaw

@Grimshaw ดังที่ฉันได้กล่าวไว้ในคำตอบสถาปัตยกรรมของคุณไม่รับประกันว่าจะให้ผลลัพธ์ที่ดีกว่ารูปแบบการจัดสรรปกติ เนื่องจากคุณไม่รู้จักรูปแบบการเข้าถึงแอปพลิเคชันของคุณจริงๆ การเพิ่มประสิทธิภาพดังกล่าวมักจะทำหลังจากการศึกษา / หลักฐานบางอย่าง สำหรับข้อเสนอแนะของฉันให้เก็บส่วนประกอบที่เกี่ยวข้องไว้ด้วยกันในหน่วยความจำเดียวกันและส่วนประกอบอื่น ๆ ในตำแหน่งที่ตั้งอื่น นี่คือพื้นกลางระหว่างทั้งหมดหรือไม่มีอะไร กระนั้นฉันก็ยังคิดว่ามันยากที่จะทำนายว่าสถาปัตยกรรมของคุณจะส่งผลต่อผลลัพธ์อย่างไรเมื่อมีเงื่อนไขเข้ามามากมาย
concept3d

ผู้ลงคะแนนเสียงสนใจอธิบายหรือไม่ เพียงชี้ปัญหาในคำตอบของฉัน ดีกว่ายังให้คำตอบที่ดีกว่า
concept3d

1

คำถามของฉันคือเนื่องจากฉันไม่ได้วนซ้ำแถวลำดับที่ต่อเนื่องกันในแต่ละครั้งในกรณีเหล่านี้ฉันจะเสียสละประสิทธิภาพที่เพิ่มขึ้นทันทีจากการจัดสรรส่วนประกอบด้วยวิธีนี้หรือไม่?

โอกาสที่คุณจะได้รับแคชน้อยกว่าโดยรวมด้วยอาร์เรย์ "แนวตั้ง" แยกต่อประเภทส่วนประกอบมากกว่าการแทรกส่วนประกอบที่แนบกับเอนทิตีในบล็อกขนาด "แนวนอน" ดังนั้นควรพูด

เหตุผลก็คือเพราะก่อนอื่นการแสดง "แนวตั้ง" จะมีแนวโน้มที่จะใช้หน่วยความจำน้อยลง คุณไม่ต้องกังวลเกี่ยวกับการจัดตำแหน่งสำหรับอาร์เรย์ที่เป็นเนื้อเดียวกันที่จัดสรรอย่างต่อเนื่อง ด้วยประเภทที่ไม่เป็นเนื้อเดียวกันที่จัดสรรลงในพูลหน่วยความจำคุณต้องกังวลเกี่ยวกับการจัดตำแหน่งเนื่องจากองค์ประกอบแรกในอาร์เรย์อาจมีขนาดและข้อกำหนดการจัดตำแหน่งที่แตกต่างกันโดยสิ้นเชิงจากวินาที เป็นผลให้คุณมักจะต้องเพิ่มการขยายเช่นในตัวอย่างง่ายๆ:

// 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;
};

สมมติว่าเราต้องการที่จะแทรกFooและBarและเก็บไว้ขวาถัดจากแต่ละอื่น ๆ ในหน่วยความจำ:

// 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;
};

ตอนนี้แทนที่จะใช้ 18 ไบต์เพื่อจัดเก็บ Foo และ Bar ในพื้นที่หน่วยความจำแยกต่างหากมันใช้เวลา 24 ไบต์ในการหลอมรวม ไม่สำคัญว่าคุณจะเปลี่ยนคำสั่งซื้อหรือไม่:

// 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'
};

หากคุณใช้หน่วยความจำมากขึ้นในบริบทการเข้าถึงตามลำดับโดยไม่มีการปรับปรุงรูปแบบการเข้าถึงอย่างมีนัยสำคัญโดยทั่วไปแล้วคุณจะได้รับแคชเพิ่มขึ้น ยิ่งไปกว่านั้นความก้าวหน้าในการรับจากเอนทิตีหนึ่งไปสู่การเพิ่มขึ้นต่อไปและขนาดของตัวแปรทำให้คุณต้องใช้ leaps ขนาดแปรผันในหน่วยความจำเพื่อรับจากเอนทิตีหนึ่งไปยังอีกถัดไปเพื่อดูว่าอันไหนมีส่วนประกอบที่คุณ ' กำลังสนใจใน

ดังนั้นการใช้การเป็นตัวแทน "แนวตั้ง" ในขณะที่คุณทำการจัดเก็บประเภทส่วนประกอบมีแนวโน้มที่จะดีที่สุดกว่าทางเลือก "แนวนอน" ที่กล่าวว่าปัญหาของแคชที่หายไปจากการเป็นตัวแทนแนวตั้งสามารถยกตัวอย่างได้ที่นี่:

ป้อนคำอธิบายรูปภาพที่นี่

ในกรณีที่ลูกศรระบุว่าเอนทิตี "เป็นเจ้าของ" ส่วนประกอบ เราสามารถเห็นได้ว่าถ้าเราพยายามเข้าถึงการเคลื่อนไหวและการเรนเดอร์องค์ประกอบของเอนทิตีที่มีทั้งคู่เราจะกระโดดไปทั่วสถานที่ในหน่วยความจำ รูปแบบการเข้าใช้แบบประปรายประเภทนั้นสามารถให้คุณโหลดข้อมูลลงในสายแคชเพื่อเข้าใช้งานกล่าวคือส่วนประกอบการเคลื่อนไหวจากนั้นเข้าถึงส่วนประกอบเพิ่มเติมและมีข้อมูลเก่าที่ถูกขับไล่เท่านั้นที่จะโหลดพื้นที่หน่วยความจำเดียวกันอีกครั้ง ส่วนประกอบ เพื่อที่จะเป็นการสิ้นเปลืองมากในการโหลดพื้นที่หน่วยความจำเดียวกันที่แน่นอนมากกว่าหนึ่งครั้งลงในแคชไลน์เพื่อวนดูและเข้าถึงรายการส่วนประกอบ

มาทำความสะอาดความยุ่งเหยิงกันสักหน่อยเพื่อให้เราสามารถมองเห็นได้ชัดเจนขึ้น:

ป้อนคำอธิบายรูปภาพที่นี่

โปรดทราบว่าหากคุณพบสถานการณ์ประเภทนี้โดยปกติจะใช้เวลานานหลังจากที่เกมเริ่มทำงานหลังจากที่มีการเพิ่มและลบส่วนประกอบและเอนทิตีหลายรายการ โดยทั่วไปเมื่อเกมเริ่มต้นขึ้นคุณอาจเพิ่มเอนทิตีและส่วนประกอบที่เกี่ยวข้องทั้งหมดเข้าด้วยกันซึ่งเป็นจุดที่พวกเขาอาจมีรูปแบบการเข้าถึงต่อเนื่องที่เป็นระเบียบและมีตำแหน่งเชิงพื้นที่ที่ดี แม้ว่าจะมีการลบและการแทรกจำนวนมาก แต่คุณอาจได้รับสิ่งที่ไม่เป็นระเบียบดังกล่าว

วิธีที่ง่ายมากในการปรับปรุงสถานการณ์นั้นก็คือการเรียงลำดับส่วนประกอบของคุณตามเลขประจำตัว / ดัชนีที่เป็นเจ้าของ ณ จุดนี้คุณจะได้รับสิ่งนี้:

ป้อนคำอธิบายรูปภาพที่นี่

และนั่นเป็นรูปแบบการเข้าถึงที่เป็นมิตรกับแคชมากกว่า ไม่สมบูรณ์เนื่องจากเราเห็นว่าเราต้องข้ามองค์ประกอบการเรนเดอร์และโมชั่นบางอย่างที่นี่และเนื่องจากระบบของเราสนใจเฉพาะเอนทิตีที่มีทั้งสองอย่างและบางเอนทิตีมีองค์ประกอบการเคลื่อนไหวและบางส่วนมีองค์ประกอบการเรนเดอร์ แต่อย่างน้อยคุณก็สามารถประมวลผลองค์ประกอบที่ต่อเนื่องกันได้ (โดยทั่วไปแล้วเนื่องจากคุณมักจะแนบองค์ประกอบที่น่าสนใจเช่นเอนทิตี้อื่น ๆ ในระบบของคุณที่มีองค์ประกอบการเคลื่อนไหวจะมีองค์ประกอบการแสดงผลมากกว่า ไม่).

สิ่งสำคัญที่สุดคือเมื่อคุณจัดเรียงข้อมูลเหล่านี้แล้วคุณจะไม่โหลดข้อมูลในส่วนหน่วยความจำลงในบรรทัดแคชเท่านั้นจากนั้นโหลดซ้ำในลูปเดียว

และนี่ไม่จำเป็นต้องมีการออกแบบที่ซับซ้อนมากนักเพียงแค่เรียงเรดิกซ์เชิงเส้นเวลาผ่านไปเรื่อย ๆ บางทีหลังจากที่คุณแทรกและลบส่วนประกอบสำหรับประเภทองค์ประกอบเฉพาะ ณ จุดที่คุณสามารถทำเครื่องหมายว่า จำเป็นต้องจัดเรียง การจัดเรียง radix ที่ใช้งานอย่างมีเหตุผล (คุณสามารถทำขนานมันซึ่งฉันทำได้) สามารถเรียงลำดับองค์ประกอบล้านชิ้นในเวลาประมาณ 6ms บน quad-core i7 ของฉันดังที่อธิบายไว้ที่นี่:

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 ]

ข้างต้นคือการจัดเรียงองค์ประกอบล้านองค์ประกอบ 32 ครั้ง (รวมถึงเวลาในmemcpyการผลลัพธ์ก่อนและหลังการเรียงลำดับ) และฉันคิดว่าเวลาส่วนใหญ่คุณจะไม่มีส่วนประกอบมากกว่าหนึ่งล้านชิ้นที่จะเรียงลำดับดังนั้นคุณควรจะสามารถแอบดูได้อย่างง่ายดายในตอนนี้และที่นั่นโดยไม่ทำให้เกิดเฟรม stutters อัตราที่เห็นได้ชัดเจน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.