แต่ OOP นี้อาจเป็นข้อเสียของซอฟต์แวร์ที่ขึ้นอยู่กับประสิทธิภาพนั่นคือโปรแกรมรันเร็วแค่ไหน?
บ่อยครั้งที่ !!! แต่...
กล่าวอีกนัยหนึ่งการอ้างอิงจำนวนมากระหว่างวัตถุที่แตกต่างกันหลายอย่างหรือใช้วิธีการมากมายจากหลายชั้นเรียนส่งผลให้เกิดการใช้งานที่ "หนัก"
ไม่จำเป็น. ขึ้นอยู่กับภาษา / คอมไพเลอร์ ตัวอย่างเช่นคอมไพเลอร์การเพิ่มประสิทธิภาพ C ++ โดยที่คุณไม่ได้ใช้ฟังก์ชั่นเสมือนบ่อยครั้งที่จะทำให้ค่าใช้จ่ายวัตถุของคุณเป็นศูนย์ คุณสามารถทำสิ่งต่าง ๆ เช่นเขียน wrapper ผ่านจุดint
นั้นหรือตัวชี้สมาร์ทที่กำหนดขอบเขตเหนือตัวชี้แบบเก่าธรรมดาซึ่งทำงานได้เร็วเท่ากับการใช้ชนิดข้อมูลแบบเก่าเหล่านี้โดยตรง
ในภาษาอื่น ๆ เช่น Java มีค่าใช้จ่ายเล็กน้อยสำหรับวัตถุ (มักจะค่อนข้างเล็กในหลายกรณี แต่ดาราศาสตร์ในบางกรณีที่หายากที่มีวัตถุเล็กจริงๆ) ตัวอย่างเช่นInteger
มีประสิทธิภาพน้อยกว่ามากint
(ใช้ 16 ไบต์เมื่อเทียบกับ 4 ใน 64- บิต) แต่นี่ไม่ใช่แค่ของเสียที่เห็นได้ชัดหรืออะไรทำนองนั้น ในการแลกเปลี่ยนข้อเสนอ Java สิ่งที่ต้องการสะท้อนให้เห็นในทุกประเภทที่ผู้ใช้กำหนดเดียวเหมือนกันเช่นเดียวกับความสามารถในการแทนที่ฟังก์ชันใด ๆ final
ที่ไม่ทำเครื่องหมายเป็น
ทีนี้มาลองดูสถานการณ์ที่ดีที่สุด: คอมไพเลอร์ที่ปรับค่า C ++ ให้เหมาะสมซึ่งสามารถปรับอินเทอร์เฟซของวัตถุให้เป็นค่าใช้จ่ายให้เป็นศูนย์ได้ ถึงแม้ว่า OOP จะลดประสิทธิภาพลงและป้องกันไม่ให้ถึงจุดสูงสุด นั่นอาจฟังดูเป็นเส้นขนานที่สมบูรณ์: เป็นไปได้อย่างไร? ปัญหาอยู่ใน:
การออกแบบส่วนต่อประสานและ Encapsulation
ปัญหาคือแม้ว่าคอมไพเลอร์สามารถสควอชโครงสร้างของวัตถุลงไปที่ศูนย์ค่าใช้จ่าย (ซึ่งอย่างน้อยบ่อยมากจริงสำหรับการเพิ่มประสิทธิภาพคอมไพเลอร์ C ++), การห่อหุ้มและการออกแบบอินเตอร์เฟส (และสะสมพึ่งพา) ของวัตถุที่ละเอียด การนำเสนอข้อมูลที่ดีที่สุดสำหรับวัตถุที่ตั้งใจจะรวบรวมโดยมวลชน (ซึ่งมักเป็นกรณีของซอฟต์แวร์ที่มีประสิทธิภาพสูง)
ใช้ตัวอย่างนี้:
class Particle
{
public:
...
private:
double birth; // 8 bytes
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
/*padding*/ // 4 bytes of padding
};
Particle particles[1000000]; // 1mil particles (~24 megs)
สมมติว่ารูปแบบการเข้าถึงหน่วยความจำของเราคือวนรอบอนุภาคเหล่านี้ตามลำดับและย้ายพวกเขาไปรอบ ๆ แต่ละเฟรมซ้ำ ๆ พวกเขาเด้งออกจากมุมของหน้าจอแล้วแสดงผล
เราสามารถเห็นค่า padding 4 ไบต์ที่จ้องมองเพื่อจัดbirth
สมาชิกให้ถูกต้องเมื่ออนุภาคถูกรวมเข้าด้วยกันอย่างต่อเนื่อง มีหน่วยความจำเหลืออยู่แล้วประมาณ 16.7% โดยใช้พื้นที่ว่างในการจัดตำแหน่ง
สิ่งนี้อาจจะดูน่าสงสัยเพราะเรามีกิกะไบต์ของ DRAM ในปัจจุบัน แม้กระทั่งเครื่องจักรที่ร้ายกาจที่สุดที่เรามีอยู่ในปัจจุบันมักจะมีเพียง 8 เมกะไบต์เท่านั้นเมื่อมันมาถึงขอบเขตที่ช้าที่สุดและใหญ่ที่สุดของ CPU แคช (L3) ยิ่งเราใส่ได้น้อยเท่าไหร่เราก็ยิ่งจ่ายมากขึ้นในแง่ของการเข้าถึง DRAM ซ้ำ ๆ และสิ่งที่ช้าลงก็จะได้รับ ทันใดนั้นการสูญเสียความทรงจำ 16.7% ดูเหมือนว่าจะไม่ยุ่งยากอีกต่อไป
เราสามารถกำจัดค่าใช้จ่ายนี้ได้อย่างง่ายดายโดยไม่มีผลกระทบใด ๆ กับการจัดแนวสนาม:
class Particle
{
public:
...
private:
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
};
Particle particles[1000000]; // 1mil particles (~12 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
ตอนนี้เราได้ลดหน่วยความจำจาก 24 megs เป็น 20 megs ด้วยรูปแบบการเข้าถึงแบบลำดับเครื่องจะใช้ข้อมูลนี้เร็วขึ้นเล็กน้อย
แต่ลองดูที่birth
ฟิลด์นี้ให้ละเอียดยิ่งขึ้น สมมติว่ามันบันทึกเวลาเริ่มต้นเมื่อเกิดอนุภาค (สร้าง) จินตนาการว่ามีการเข้าถึงสนามเมื่อมีการสร้างอนุภาคครั้งแรกและทุก ๆ 10 วินาทีเพื่อดูว่าอนุภาคควรตายและเกิดใหม่ในตำแหน่งสุ่มบนหน้าจอ ในกรณีbirth
นี้เป็นเขตเย็น ไม่สามารถเข้าถึงได้ในลูปประสิทธิภาพที่สำคัญของเรา
ด้วยเหตุนี้ข้อมูลสำคัญของประสิทธิภาพจึงไม่ใช่ 20 เมกะไบต์ แต่จริงๆแล้วเป็นบล็อกที่ต่อเนื่อง 12 เมกะไบต์ หน่วยความจำร้อนที่เกิดขึ้นจริงที่เรากำลังเข้าถึงบ่อยได้หดไปครึ่งขนาดของมัน! คาดหวังว่าจะเร่งความเร็วอย่างรวดเร็วเหนือโซลูชันขนาด 24 เมกะไบต์ดั้งเดิมของเรา (ไม่จำเป็นต้องวัด - ทำสิ่งนี้ได้หลายพันครั้งแล้ว แต่อย่าลังเลถ้าสงสัย)
แต่สังเกตว่าเราทำอะไรที่นี่ เราทำลายการห่อหุ้มของวัตถุอนุภาคอย่างสมบูรณ์ ตอนนี้สถานะของมันจะถูกแบ่งระหว่างParticle
เขตข้อมูลส่วนตัวของประเภทและอาร์เรย์แบบขนานแยกต่างหาก และนั่นคือสิ่งที่การออกแบบเชิงวัตถุแบบละเอียดเข้ามาขวางทาง
เราไม่สามารถแสดงข้อมูลที่ดีที่สุดได้เมื่อ จำกัด การออกแบบส่วนต่อประสานของวัตถุชิ้นเล็กชิ้นน้อยเช่นอนุภาคเดี่ยวพิกเซลเดียวแม้แต่เวกเตอร์ 4 องค์ประกอบเดียวอาจเป็นวัตถุ "สิ่งมีชีวิต" เดียวในเกม เป็นต้นความเร็วของเสือชีต้าจะสูญเปล่าหากมันอยู่บนเกาะเล็ก ๆ ที่มีขนาด 2 ตารางเมตรและนั่นคือสิ่งที่การออกแบบเชิงวัตถุที่ละเอียดมากมักจะมีประสิทธิภาพ มัน จำกัด การแสดงข้อมูลให้เป็นลักษณะย่อยที่ดีที่สุด
เพื่อให้ได้สิ่งต่อไปสมมติว่าเนื่องจากเราเพิ่งเคลื่อนที่อนุภาคไปรอบ ๆ เราสามารถเข้าถึงเขตข้อมูล x / y / z ของพวกเขาในวงแยกสามวง ในกรณีนี้เราสามารถได้รับประโยชน์จากรูปแบบ SOA SIMD ที่แท้จริงด้วย AVX รีจิสเตอร์ที่สามารถแปลงการดำเนินการ SPFP 8 แบบขนาน แต่เมื่อต้องการทำเช่นนี้เราจะต้องใช้การเป็นตัวแทนนี้:
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
ตอนนี้เรากำลังบินไปพร้อมกับการจำลองอนุภาค แต่ดูว่าเกิดอะไรขึ้นกับการออกแบบอนุภาคของเรา มันพังยับเยินอย่างสมบูรณ์และตอนนี้เรากำลังดูอาร์เรย์แบบขนาน 4 อันและไม่มีวัตถุใด ๆ ที่จะรวมพวกมันได้ Particle
การออกแบบเชิงวัตถุของเราได้ไปสู่ซาโนร่าแล้ว
สิ่งนี้เกิดขึ้นกับฉันหลายครั้งที่ทำงานในเขตข้อมูลที่มีประสิทธิภาพซึ่งผู้ใช้ต้องการความเร็วด้วยความถูกต้องเพียงอย่างเดียวซึ่งเป็นสิ่งที่พวกเขาต้องการมากขึ้น การออกแบบเชิงวัตถุเล็ก ๆ เหล่านี้จะต้องพังยับเยินและการแยกเรียงซ้อนมักต้องการให้เราใช้กลยุทธ์การคัดค้านอย่างช้า ๆ ไปสู่การออกแบบที่เร็วขึ้น
วิธีการแก้
สถานการณ์ดังกล่าวข้างต้นเพียงนำเสนอปัญหากับเม็ดการออกแบบเชิงวัตถุ ในกรณีดังกล่าวเรามักจะต้องทำลายโครงสร้างเพื่อแสดงการแทนที่มีประสิทธิภาพมากขึ้นซึ่งเป็นผลมาจากตัวแทนของ SoA การแยกเขตร้อน / เย็นการลดช่องว่างสำหรับรูปแบบการเข้าถึงตามลำดับ รูปแบบในกรณี AoS แต่เกือบจะเป็นอุปสรรคสำหรับรูปแบบการเข้าถึงตามลำดับ) และอื่น ๆ
แต่เราสามารถนำเสนอสุดท้ายที่เราตัดสินและยังคงจำลองอินเทอร์เฟซเชิงวัตถุ:
// Represents a collection of particles.
class ParticleSystem
{
public:
...
private:
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
};
ตอนนี้เราดี เราสามารถได้รับของสารพัดเชิงวัตถุที่เราชอบ เสือชีต้ามีทั้งประเทศให้วิ่งเร็วเท่าที่จะทำได้ อินเทอร์เฟซของเราออกแบบไม่ให้ดักจับมุมคอขวดอีกต่อไป
ParticleSystem
อาจเป็นนามธรรมและใช้ฟังก์ชันเสมือนได้ มันสงสัยตอนนี้เรากำลังจ่ายเงินสำหรับค่าใช้จ่ายในคอลเลกชันของอนุภาคระดับแทนการที่ต่ออนุภาคระดับ ค่าโสหุ้ยคือ 1 / 1,000,000 ในสิ่งที่มันจะเป็นอย่างอื่นถ้าเราจำลองวัตถุในระดับอนุภาคของแต่ละบุคคล
นั่นคือคำตอบในพื้นที่ที่มีความสำคัญต่อประสิทธิภาพอย่างแท้จริงซึ่งรองรับภาระงานหนักและสำหรับภาษาการเขียนโปรแกรมทุกประเภท (เทคนิคนี้มีประโยชน์กับ C, C ++, Python, Java, JavaScript, Lua, Swift และอื่น ๆ ) และมันก็ไม่สามารถจะระบุว่าเป็น "การเพิ่มประสิทธิภาพก่อนวัยอันควร" ตั้งแต่นี้เกี่ยวข้องกับการออกแบบอินเตอร์เฟซและสถาปัตยกรรม เราไม่สามารถเขียน codebase แบบจำลองของอนุภาคเดี่ยวเป็นวัตถุที่มีจำนวนเรือบรรทุกของการพึ่งพาลูกค้าไปยังParticle's
ส่วนต่อประสานสาธารณะแล้วเปลี่ยนใจในภายหลัง ฉันได้ทำสิ่งนั้นมาหลายครั้งเมื่อถูกเรียกให้ปรับแต่งรหัสฐานแบบดั้งเดิมและนั่นอาจใช้เวลาหลายเดือนในการเขียนบรรทัดใหม่อีกนับหมื่นบรรทัดอย่างระมัดระวังเพื่อใช้การออกแบบที่มีขนาดใหญ่ขึ้น สิ่งนี้มีผลต่อวิธีที่เราออกแบบสิ่งต่าง ๆ ล่วงหน้าโดยที่เราสามารถคาดการณ์ภาระหนักได้
ฉันยังคงสะท้อนคำตอบนี้ในบางรูปแบบหรืออีกคำถามหนึ่งในคำถามด้านประสิทธิภาพมากมายโดยเฉพาะคำถามที่เกี่ยวข้องกับการออกแบบเชิงวัตถุ การออกแบบเชิงวัตถุยังสามารถใช้งานร่วมกับความต้องการประสิทธิภาพสูงสุดได้ แต่เราต้องเปลี่ยนวิธีที่เราคิดเกี่ยวกับมันเล็กน้อย เราต้องให้เสือชีตาห์บางห้องวิ่งเร็วเท่าที่จะทำได้และบ่อยครั้งที่เป็นไปไม่ได้ถ้าเราออกแบบวัตถุเล็ก ๆ น้อย ๆ ที่แทบจะไม่เก็บสถานะใด ๆ เลย