ความแตกต่างระหว่าง " รหัสที่ไม่เป็นมิตรแคช " และ " รหัสที่เป็นมิตรกับแคช " คืออะไร?
ฉันจะแน่ใจได้อย่างไรว่าฉันเขียนโค้ดที่มีประสิทธิภาพแคช
ความแตกต่างระหว่าง " รหัสที่ไม่เป็นมิตรแคช " และ " รหัสที่เป็นมิตรกับแคช " คืออะไร?
ฉันจะแน่ใจได้อย่างไรว่าฉันเขียนโค้ดที่มีประสิทธิภาพแคช
คำตอบ:
บนคอมพิวเตอร์สมัยใหม่เฉพาะโครงสร้างหน่วยความจำระดับต่ำสุด (เรจิสเตอร์ ) เท่านั้นที่สามารถย้ายข้อมูลในรอบนาฬิกาเดียว อย่างไรก็ตามการลงทะเบียนมีราคาแพงมากและคอร์คอมพิวเตอร์ส่วนใหญ่มีการลงทะเบียนน้อยกว่าสองสามโหล (ไม่กี่ร้อยถึงอาจรวมเป็นพันไบต์ ) ที่ปลายอีกด้านหนึ่งของหน่วยความจำสเปกตรัม ( DRAM ) หน่วยความจำราคาถูกมาก (เช่นแท้จริงถูกกว่าล้านครั้ง ) แต่ใช้เวลาหลายร้อยรอบหลังจากขอให้ได้รับข้อมูล ในการลดช่องว่างนี้ระหว่างความรวดเร็วและแพงและซุปเปอร์ช้าและราคาถูกนั้นเป็นความทรงจำแคชชื่อ L1, L2, L3 ในการลดความเร็วและค่าใช้จ่าย แนวคิดคือรหัสการดำเนินการส่วนใหญ่จะตีชุดตัวแปรขนาดเล็กบ่อยครั้งและส่วนที่เหลือ (ชุดตัวแปรขนาดใหญ่กว่า) นาน ๆ ครั้ง หากโปรเซสเซอร์ไม่พบข้อมูลในแคช L1 แสดงว่าแคช L2 นั้นมีลักษณะ หากไม่ได้อยู่ที่นั่นแคช L3 และหากไม่มีหน่วยความจำหลัก แต่ละ "คิดถึง" เหล่านี้มีราคาแพงในเวลา
(การเปรียบเทียบคือหน่วยความจำแคชคือหน่วยความจำระบบเนื่องจากหน่วยความจำระบบจัดเก็บข้อมูลบนฮาร์ดดิสก์มากเกินไปการจัดเก็บฮาร์ดดิสก์ราคาถูกสุด แต่ช้ามาก)
แคชเป็นหนึ่งในวิธีการหลักในการลดผลกระทบจากความล่าช้า ในการถอดความ Herb Sutter (ลิงก์ลิงก์ด้านล่าง): การเพิ่มแบนด์วิดท์เป็นเรื่องง่าย แต่เราไม่สามารถซื้อหน่วงเวลาได้
ข้อมูลจะถูกดึงผ่านลำดับชั้นหน่วยความจำเสมอ (น้อยที่สุด == เร็วที่สุดไปช้าที่สุด) แคชตี / นางสาวมักจะหมายถึงการตี / พลาดในระดับสูงสุดของแคชในซีพียู - จากระดับสูงสุดที่ผมหมายถึงที่ใหญ่ที่สุด == ช้าที่สุด อัตราแคชตีเป็นสิ่งสำคัญสำหรับผลการดำเนินงานตั้งแต่ทุกแคชผลการพลาดในข้อมูลการดึงข้อมูลจาก RAM (หรือแย่ลง ... ) ซึ่งจะใช้เวลามากของเวลา (หลายร้อยรอบสำหรับแรมหลายสิบล้านของรอบสำหรับ HDD) ในการเปรียบเทียบการอ่านข้อมูลจากแคช (ระดับสูงสุด) มักใช้เวลาเพียงไม่กี่รอบ
ในสถาปัตยกรรมคอมพิวเตอร์สมัยใหม่คอขวดของประสิทธิภาพกำลังทำให้ CPU ตาย (เช่นการเข้าถึง RAM หรือสูงกว่า) สิ่งนี้จะเลวร้ายลงเมื่อเวลาผ่านไป การเพิ่มขึ้นของความถี่โปรเซสเซอร์ในปัจจุบันไม่เกี่ยวข้องกับการเพิ่มประสิทธิภาพอีกต่อไป ปัญหาคือการเข้าถึงหน่วยความจำ ความพยายามในการออกแบบฮาร์ดแวร์ในซีพียูในปัจจุบันจึงมุ่งเน้นไปที่การเพิ่มประสิทธิภาพแคชการดึงข้อมูลล่วงหน้าท่อและการทำงานพร้อมกัน ยกตัวอย่างเช่นซีพียูสมัยใหม่ใช้จ่ายแคช 85% ของค่าตายและ 99% สำหรับการจัดเก็บ / ย้ายข้อมูล!
มีจำนวนมากที่จะพูดเกี่ยวกับเรื่องนี้ ต่อไปนี้เป็นข้อมูลอ้างอิงที่ดีเกี่ยวกับแคชลำดับชั้นหน่วยความจำและการเขียนโปรแกรมที่เหมาะสม:
สิ่งที่สำคัญมากของรหัสที่เป็นมิตรกับแคชคือทั้งหมดที่เกี่ยวกับหลักการของพื้นที่เป้าหมายคือการวางข้อมูลที่เกี่ยวข้องไว้ในหน่วยความจำเพื่อให้การแคชมีประสิทธิภาพ ในแง่ของแคช CPU สิ่งสำคัญที่ควรระวังคือบรรทัดแคชเพื่อทำความเข้าใจวิธีการทำงาน: บรรทัดแคชทำงานอย่างไร
ลักษณะเฉพาะต่อไปนี้มีความสำคัญสูงในการปรับการแคชให้เหมาะสม:
ใช้อย่างเหมาะสม C ++ ตู้คอนเทนเนอร์
ตัวอย่างง่ายๆของการแคชที่เป็นมิตรกับการแคชที่ไม่เป็นมิตรคือ C ++'s เมื่อเทียบกับstd::vector
std::list
องค์ประกอบของการstd::vector
จะถูกเก็บไว้ในหน่วยความจำที่อยู่ติดกันและเป็นเช่นการเข้าถึงพวกเขาเป็นมากเพิ่มเติมแคชง่ายกว่าการเข้าถึงองค์ประกอบในstd::list
ที่เก็บเนื้อหาทั่วทุกสถานที่ นี่เป็นเพราะพื้นที่ท้องถิ่น
ภาพประกอบที่ยอดเยี่ยมนี้มอบให้โดย Bjarne Stroustrup ในคลิป youtube นี้ (ขอบคุณ @ Mohammad Ali Baydoun สำหรับลิงก์!)
อย่าละเลยแคชในโครงสร้างข้อมูลและการออกแบบอัลกอริทึม
เมื่อใดก็ตามที่เป็นไปได้ให้พยายามปรับโครงสร้างข้อมูลและลำดับการคำนวณด้วยวิธีที่ช่วยให้สามารถใช้งานแคชได้สูงสุด เทคนิคทั่วไปในเรื่องนี้คือการบล็อกแคช (เวอร์ชั่น Archive.org)ซึ่งมีความสำคัญอย่างยิ่งในการคำนวณประสิทธิภาพสูง (cfr. เช่นATLAS )
รู้และใช้ประโยชน์จากโครงสร้างข้อมูลโดยปริยาย
อีกตัวอย่างง่ายๆที่หลายคนในสาขาลืมบางครั้งก็คือคอลัมน์ใหญ่ (เช่น Fortran,MATLAB) เทียบกับการสั่งซื้อแถวหลัก (เช่น ค,C ++) สำหรับการจัดเก็บอาร์เรย์สองมิติ ตัวอย่างเช่นพิจารณาเมทริกซ์ต่อไปนี้:
1 2
3 4
ในการเรียงลำดับแถวหลักจะถูกเก็บไว้ในหน่วยความจำ1 2 3 4
ดังนี้ 1 3 2 4
ในการสั่งซื้อคอลัมน์ที่สำคัญนี้จะถูกเก็บไว้เป็น เป็นเรื่องง่ายที่จะเห็นว่าการใช้งานที่ไม่ใช้ประโยชน์จากคำสั่งนี้จะประสบปัญหาแคช (หลีกเลี่ยงได้ง่าย!) แต่น่าเสียดายที่ฉันเห็นสิ่งเช่นนี้มากมักจะอยู่ในโดเมน (เรียนรู้ของเครื่อง) ของฉัน @MatteoItalia แสดงตัวอย่างนี้โดยละเอียดยิ่งขึ้นในคำตอบของเขา
เมื่อดึงองค์ประกอบบางอย่างของเมทริกซ์จากหน่วยความจำองค์ประกอบที่อยู่ใกล้จะถูกดึงเช่นกันและเก็บไว้ในสายแคช หากการสั่งซื้อถูกใช้ประโยชน์สิ่งนี้จะส่งผลให้มีการเข้าถึงหน่วยความจำน้อยลง (เนื่องจากมีค่าน้อยมากที่จำเป็นสำหรับการคำนวณในลำดับต่อมาที่มีอยู่ในแคช)
เพื่อความง่ายสมมติว่าแคชประกอบด้วยบรรทัดแคชเดี่ยวซึ่งสามารถมีองค์ประกอบเมทริกซ์ได้ 2 รายการและเมื่อองค์ประกอบที่กำหนดถูกดึงมาจากหน่วยความจำส่วนถัดไปก็เช่นกัน สมมติว่าเราต้องการหาผลรวมเหนือองค์ประกอบทั้งหมดในตัวอย่าง 2x2 เมทริกซ์ด้านบน (เรียกว่าM
):
ใช้ประโยชน์จากการสั่งซื้อ (เช่นการเปลี่ยนดัชนีคอลัมน์ก่อน C ++):
M[0][0] (memory) + M[0][1] (cached) + M[1][0] (memory) + M[1][1] (cached)
= 1 + 2 + 3 + 4
--> 2 cache hits, 2 memory accesses
ไม่ใช้ประโยชน์จากการจัดลำดับ (เช่นการเปลี่ยนดัชนีแถวก่อน C ++):
M[0][0] (memory) + M[1][0] (memory) + M[0][1] (memory) + M[1][1] (memory)
= 1 + 3 + 2 + 4
--> 0 cache hits, 4 memory accesses
ในตัวอย่างง่ายๆนี้การใช้ประโยชน์จากการสั่งซื้อความเร็วในการประมวลผลประมาณสองเท่า (เนื่องจากการเข้าถึงหน่วยความจำต้องใช้รอบมากกว่าการคำนวณจำนวนเงิน) ในทางปฏิบัติแตกต่างประสิทธิภาพสามารถมากขนาดใหญ่
หลีกเลี่ยงกิ่งที่ไม่สามารถคาดเดาได้
สถาปัตยกรรมสมัยใหม่มีคุณสมบัติท่อและคอมไพเลอร์กำลังดีในการจัดเรียงรหัสใหม่เพื่อลดความล่าช้าเนื่องจากการเข้าถึงหน่วยความจำ เมื่อรหัสสำคัญของคุณมีสาขา (คาดเดาไม่ได้) มันยากหรือเป็นไปไม่ได้ที่จะดึงข้อมูลล่วงหน้า สิ่งนี้จะนำไปสู่การแคชที่ขาดหายไปทางอ้อมมากขึ้น
สิ่งนี้อธิบายได้ดีมากที่นี่ (ขอบคุณ @ 0x90 สำหรับลิงก์): เหตุใดการประมวลผลอาร์เรย์ที่เรียงลำดับจึงเร็วกว่าการประมวลผลอาร์เรย์ที่ไม่เรียงลำดับ
หลีกเลี่ยงฟังก์ชั่นเสมือนจริง
ในบริบทของ C ++, virtual
วิธีการเป็นตัวแทนของปัญหาที่ถกเถียงกันในเรื่องเกี่ยวกับแคช (ฉันทามติทั่วไปอยู่แล้วว่าพวกเขาควรหลีกเลี่ยงเมื่อเป็นไปได้ในแง่ของประสิทธิภาพ) ด้วย ฟังก์ชั่นเสมือนสามารถชักนำให้เกิดการพลาดแคชในระหว่างการค้นหา แต่สิ่งนี้จะเกิดขึ้นเฉพาะในกรณีที่ฟังก์ชั่นเฉพาะไม่ได้ถูกเรียกบ่อยครั้ง (มิเช่นนั้นอาจจะถูกแคช) สำหรับการอ้างอิงเกี่ยวกับปัญหานี้ให้ตรวจสอบ: ราคาประสิทธิภาพของการมีวิธีเสมือนในคลาส C ++ คืออะไร
ปัญหาที่พบบ่อยในสถาปัตยกรรมที่ทันสมัยด้วยแคชมัลติโปรเซสเซอร์ที่เรียกว่าร่วมกันเท็จ นี้เกิดขึ้นเมื่อโปรเซสเซอร์แต่ละบุคคลจะพยายามที่จะใช้ข้อมูลในหน่วยความจำภูมิภาคอื่นและพยายามที่จะเก็บไว้ในเดียวกันบรรทัดแคช สิ่งนี้ทำให้สายแคช - ซึ่งมีข้อมูลที่ตัวประมวลผลอื่นสามารถใช้ - ถูกเขียนทับซ้ำแล้วซ้ำอีก อย่างมีประสิทธิภาพกระทู้ที่แตกต่างกันทำให้การรอคอยกันโดยชักนำให้เกิดแคชที่ขาดหายไปในสถานการณ์นี้ ดูเพิ่มเติม (ขอบคุณ @Matt สำหรับลิงก์): จะกำหนดขนาดแคชของบรรทัดได้อย่างไรและเมื่อไหร่?
อาการที่รุนแรงของแคชยากจนในหน่วยความจำแรม (ซึ่งอาจจะไม่ใช่สิ่งที่คุณหมายถึงในบริบทนี้คือ) เป็นสิ่งที่เรียกว่าหวด สิ่งนี้เกิดขึ้นเมื่อกระบวนการสร้างข้อบกพร่องของหน้าอย่างต่อเนื่อง (เช่นเข้าถึงหน่วยความจำที่ไม่ได้อยู่ในหน้าปัจจุบัน) ซึ่งต้องใช้การเข้าถึงดิสก์
นอกจากคำตอบของ @Marc Claesen ฉันคิดว่าตัวอย่างคลาสสิกที่ให้คำแนะนำของรหัสแคชที่ไม่เป็นมิตรคือโค้ดที่สแกนอาร์เรย์ C สองมิติ (เช่นรูปภาพบิตแมป) แทนที่จะเป็นแถวที่ชาญฉลาด
องค์ประกอบที่อยู่ติดกันในแถวนั้นก็อยู่ติดกันในหน่วยความจำด้วยดังนั้นการเข้าถึงตามลำดับหมายถึงการเข้าถึงองค์ประกอบเหล่านั้นตามลำดับหน่วยความจำจากน้อยไปมาก นี่เป็นมิตรกับแคชเนื่องจากแคชมีแนวโน้มที่จะดึงบล็อกของหน่วยความจำที่ต่อเนื่องกันล่วงหน้า
แต่การเข้าถึงองค์ประกอบดังกล่าวคอลัมน์ที่ชาญฉลาดคือแคชที่ไม่เป็นมิตรเนื่องจากองค์ประกอบในคอลัมน์เดียวกันนั้นอยู่ไกลในหน่วยความจำซึ่งกันและกัน (โดยเฉพาะระยะห่างของพวกเขาจะเท่ากับขนาดของแถว) ดังนั้นเมื่อคุณใช้รูปแบบการเข้าถึงนี้ กำลังกระโดดไปรอบ ๆ ในหน่วยความจำอาจสูญเสียความพยายามของแคชในการเรียกใช้องค์ประกอบที่อยู่ใกล้เคียงในหน่วยความจำ
และทั้งหมดที่ใช้ในการทำลายประสิทธิภาพคือไปจาก
// Cache-friendly version - processes pixels which are adjacent in memory
for(unsigned int y=0; y<height; ++y)
{
for(unsigned int x=0; x<width; ++x)
{
... image[y][x] ...
}
}
ถึง
// Cache-unfriendly version - jumps around in memory for no good reason
for(unsigned int x=0; x<width; ++x)
{
for(unsigned int y=0; y<height; ++y)
{
... image[y][x] ...
}
}
เอฟเฟกต์นี้ค่อนข้างน่าทึ่ง (มีหลายลำดับความเร็วในระบบ) ในระบบที่มีแคชขนาดเล็กและ / หรือทำงานกับอาร์เรย์ขนาดใหญ่ (เช่น 10+ ล้านพิกเซล 24 bpp ภาพในเครื่องปัจจุบัน); ด้วยเหตุนี้หากคุณต้องสแกนแนวตั้งหลาย ๆ ครั้งบ่อยครั้งจะเป็นการดีกว่าที่จะหมุนภาพ 90 องศาก่อนและทำการวิเคราะห์ต่าง ๆ ในภายหลังโดย จำกัด รหัสแคชที่ไม่เป็นมิตรกับการหมุน
การเพิ่มประสิทธิภาพการใช้แคชส่วนใหญ่มาจากสองปัจจัย
ปัจจัยแรก (ซึ่งคนอื่นได้พูดพาดพิงถึง) เป็นสถานที่ของการอ้างอิง สถานที่อ้างอิงมีสองมิติจริง ๆ : พื้นที่และเวลา
มิติเชิงพื้นที่มีสองสิ่งด้วยกัน: อันดับแรกเราต้องการจัดเก็บข้อมูลของเราอย่างหนาแน่นดังนั้นข้อมูลเพิ่มเติมจะพอดีกับหน่วยความจำที่ จำกัด นั้น นี่หมายถึง (ตัวอย่าง) ที่คุณต้องการการปรับปรุงที่สำคัญในความซับซ้อนของการคำนวณเพื่อปรับโครงสร้างข้อมูลตามโหนดขนาดเล็กที่เข้าร่วมโดยพอยน์เตอร์
ประการที่สองเราต้องการข้อมูลที่จะถูกประมวลผลด้วยกันอยู่ด้วยกัน แคชทั่วไปทำงานใน "บรรทัด" ซึ่งหมายความว่าเมื่อคุณเข้าถึงข้อมูลบางส่วนข้อมูลอื่น ๆ ที่อยู่ใกล้เคียงจะถูกโหลดลงในแคชด้วยส่วนที่เราสัมผัส ตัวอย่างเช่นเมื่อฉันแตะหนึ่งไบต์แคชอาจโหลด 128 หรือ 256 ไบต์ใกล้กับที่หนึ่ง ในการใช้ประโยชน์จากสิ่งนั้นโดยทั่วไปคุณต้องการให้ข้อมูลถูกจัดเรียงเพื่อเพิ่มโอกาสที่คุณจะใช้ข้อมูลอื่น ๆ ที่ถูกโหลดในเวลาเดียวกัน
สำหรับตัวอย่างเล็ก ๆ น้อย ๆ จริง ๆ นี่อาจหมายความว่าการค้นหาเชิงเส้นสามารถแข่งขันกับการค้นหาแบบไบนารีได้มากกว่าที่คุณคาดหวัง เมื่อคุณโหลดหนึ่งรายการจากบรรทัดแคชการใช้ข้อมูลที่เหลือในบรรทัดแคชนั้นเกือบจะว่าง การค้นหาแบบไบนารีจะเร็วขึ้นอย่างเห็นได้ชัดก็ต่อเมื่อข้อมูลมีขนาดใหญ่พอที่การค้นหาแบบไบนารีจะลดจำนวนของบรรทัดแคชที่คุณเข้าถึง
มิติเวลาหมายความว่าเมื่อคุณทำการดำเนินการบางอย่างกับข้อมูลบางอย่างคุณต้องการ (มากที่สุด) เพื่อทำการดำเนินการทั้งหมดกับข้อมูลนั้นในครั้งเดียว
เนื่องจากคุณได้ติดแท็กนี้เป็น C ++ std::valarray
ผมจะชี้ไปที่ตัวอย่างคลาสสิกของการออกแบบที่ค่อนข้างแคชไม่เป็นมิตร: valarray
overloads ดำเนินการทางคณิตศาสตร์มากที่สุดเพื่อให้ฉันสามารถ (ตัวอย่าง) กล่าวa = b + c + d;
(ที่a
, b
, c
และd
มี valarrays ทั้งหมด) จะทำอย่างไรนอกจากนี้องค์ประกอบที่ชาญฉลาดของอาร์เรย์เหล่านั้น
ปัญหานี้คือว่ามันผ่านอินพุตหนึ่งคู่ทำให้ผลลัพธ์ในชั่วคราวเดินผ่านอินพุตคู่อื่นและอื่น ๆ ด้วยข้อมูลจำนวนมากผลลัพธ์จากการคำนวณหนึ่งครั้งอาจหายไปจากแคชก่อนที่จะถูกใช้ในการคำนวณครั้งถัดไปดังนั้นเราจึงสิ้นสุดการอ่าน (และเขียน) ข้อมูลซ้ำ ๆ ก่อนที่เราจะได้ผลลัพธ์สุดท้าย หากองค์ประกอบของผลสุดท้ายแต่ละคนจะเป็นสิ่งที่ชอบ(a[n] + b[n]) * (c[n] + d[n]);
โดยทั่วไปเราต้องการที่จะอ่านแต่ละa[n]
, b[n]
, c[n]
และd[n]
ครั้งเดียวทำคำนวณเขียนผลที่เพิ่มขึ้นn
และทำซ้ำจนกว่าที่เรากำลังทำ 2
ปัจจัยหลักที่สองคือการหลีกเลี่ยงการแบ่งปันสาย เพื่อให้เข้าใจสิ่งนี้เราอาจต้องสำรองข้อมูลและดูวิธีการจัดระบบแคชเล็กน้อย รูปแบบแคชที่ง่ายที่สุดถูกแมปโดยตรง ซึ่งหมายความว่าหนึ่งที่อยู่ในหน่วยความจำหลักสามารถจัดเก็บได้ในที่เดียวเท่านั้นในแคช หากเราใช้รายการข้อมูลสองรายการที่แมปไปยังจุดเดียวกันในแคชมันจะทำงานได้ไม่ดี - ทุกครั้งที่เราใช้รายการข้อมูลหนึ่งรายการอื่นจะต้องถูกล้างออกจากแคชเพื่อให้มีที่ว่างอีกรายการหนึ่ง ส่วนที่เหลือของแคชอาจว่างเปล่า แต่รายการเหล่านั้นจะไม่ใช้ส่วนอื่น ๆ ของแคช
เพื่อป้องกันปัญหานี้แคชส่วนใหญ่จึงเป็นสิ่งที่เรียกว่า "ชุดเชื่อมโยง" ตัวอย่างเช่นในแคชการตั้งค่าการเชื่อมโยง 4 ทิศทางรายการใด ๆ จากหน่วยความจำหลักสามารถเก็บไว้ที่ใดก็ได้ใน 4 ตำแหน่งที่แตกต่างกันในแคช ดังนั้นเมื่อแคชเป็นไปโหลดรายการนี้จะมองหาน้อยเพิ่งใช้3รายการในบรรดาสี่วูบวาบไปยังหน่วยความจำหลักและโหลดรายการใหม่ในสถานที่
ปัญหาน่าจะค่อนข้างชัดเจน: สำหรับแคชที่แมปโดยตรงตัวถูกดำเนินการสองตัวที่เกิดขึ้นกับแมปไปยังตำแหน่งแคชเดียวกันอาจทำให้เกิดพฤติกรรมที่ไม่ดี N-way set-associative cache เพิ่มจำนวนจาก 2 เป็น N + 1 การจัดระเบียบแคชให้เป็น "วิธี" มากขึ้นนั้นต้องใช้วงจรเพิ่มเติมและโดยทั่วไปจะทำงานช้าลงดังนั้น (ตัวอย่างเช่น) แคชที่เชื่อมโยงชุด 8192- ทางเป็นวิธีแก้ปัญหาที่ดีเช่นกัน
ท้ายที่สุดปัจจัยนี้ยากต่อการควบคุมในรหัสพกพา การควบคุมว่าข้อมูลของคุณจะถูก จำกัด อยู่ที่ใด ยิ่งไปกว่านั้นการแมปที่แน่นอนจากที่อยู่ไปยังแคชแตกต่างกันระหว่างโปรเซสเซอร์ที่คล้ายกัน อย่างไรก็ตามในบางกรณีมันอาจคุ้มค่าที่จะทำสิ่งต่าง ๆ เช่นการจัดสรรบัฟเฟอร์ที่มีขนาดใหญ่แล้วใช้เฉพาะบางส่วนของสิ่งที่คุณจัดสรรเพื่อให้แน่ใจว่าข้อมูลที่ใช้ร่วมกันในแคชบรรทัดเดียวกัน (แม้ว่าคุณอาจต้องตรวจสอบโปรเซสเซอร์ ลงมือทำเพื่อทำสิ่งนี้)
มีอีกรายการที่เกี่ยวข้องที่เรียกว่า "การแบ่งปันที่ผิด" สิ่งนี้เกิดขึ้นในระบบมัลติโปรเซสเซอร์หรือระบบมัลติคอร์ซึ่งโปรเซสเซอร์ / คอร์สองตัว (หรือมากกว่า) มีข้อมูลที่แยกจากกัน แต่อยู่ในบรรทัดแคชเดียวกัน สิ่งนี้บังคับให้ตัวประมวลผล / แกนประมวลผลทั้งสองแกนประสานงานการเข้าถึงข้อมูลแม้ว่าแต่ละรายการจะมีรายการข้อมูลแยกต่างหาก โดยเฉพาะอย่างยิ่งหากทั้งสองปรับเปลี่ยนข้อมูลในการสลับกันสิ่งนี้อาจนำไปสู่การชะลอตัวครั้งใหญ่เนื่องจากข้อมูลต้องถูกปิดอย่างต่อเนื่องระหว่างโปรเซสเซอร์ สิ่งนี้ไม่สามารถรักษาให้หายได้ง่าย ๆ โดยการจัดแคชให้เป็น "วิธี" เพิ่มเติมหรืออะไรทำนองนั้น วิธีหลักในการป้องกันคือเพื่อให้แน่ใจว่าเธรดสองอันที่ไม่ค่อยมี (โดยเฉพาะอย่างยิ่งไม่ควร) แก้ไขข้อมูลที่อาจอยู่ในแคชบรรทัดเดียวกัน (โดยมีคำเตือนเดียวกันเกี่ยวกับความยากลำบากในการควบคุมที่อยู่
ผู้ที่รู้จัก C ++ เป็นอย่างดีอาจสงสัยว่าสิ่งนี้เปิดรับการปรับให้เหมาะสมผ่านทางเทมเพลตการแสดงออก ฉันค่อนข้างแน่ใจว่าคำตอบคือใช่มันสามารถทำได้และถ้าเป็นเช่นนั้นมันอาจจะเป็นชัยชนะที่สำคัญมาก อย่างไรก็ตามฉันไม่ทราบว่ามีใครทำเช่นนั้นและเนื่องจากการใช้เพียงเล็กน้อยvalarray
ฉันก็ต้องประหลาดใจเล็กน้อยที่เห็นใครทำเช่นนั้น
ในกรณีที่ใครสงสัยว่าvalarray
(ออกแบบมาเพื่อประสิทธิภาพโดยเฉพาะ) อาจผิดพลาดได้อย่างไรมันมีอยู่สิ่งหนึ่ง: มันถูกออกแบบมาสำหรับเครื่องเช่น Crays รุ่นเก่าที่ใช้หน่วยความจำหลักที่รวดเร็วและไม่มีแคช สำหรับพวกเขาแล้วนี่เป็นการออกแบบในอุดมคติที่เกือบจะสมบูรณ์แบบ
ใช่ฉันกำลังทำให้เข้าใจง่าย: แคชส่วนใหญ่ไม่ได้วัดรายการที่ใช้น้อยที่สุดอย่างแม่นยำ แต่พวกเขาใช้ฮิวริสติกบางอย่างที่ใกล้เคียงกับรายการโดยไม่ต้องประทับเวลาเต็มรูปแบบสำหรับการเข้าถึงแต่ละครั้ง
valarray
ตัวอย่าง
ยินดีต้อนรับสู่โลกของ Data Oriented Design มนต์ขั้นพื้นฐานคือการเรียงลำดับกำจัดสาขาแบทช์กำจัดvirtual
สาย - ขั้นตอนทั้งหมดสู่ท้องถิ่นที่ดีกว่า
เนื่องจากคุณแท็กคำถามกับ C ++ นี่คือบังคับทั่วไป c ++ โกหก ข้อผิดพลาดของ Tony Albrecht ในการเขียนโปรแกรมเชิงวัตถุยังเป็นการแนะนำที่ดีในเรื่องนี้
เพียงแค่กองพะเนิน: ตัวอย่างคลาสสิกของแคชที่ไม่เป็นมิตรกับรหัสที่เป็นมิตรกับแคชคือ "การบล็อกแคช" ของเมทริกซ์ทวีคูณ
Naive matrix ทวีคูณดูเหมือนว่า:
for(i=0;i<N;i++) {
for(j=0;j<N;j++) {
dest[i][j] = 0;
for( k==;k<N;i++) {
dest[i][j] += src1[i][k] * src2[k][j];
}
}
}
หากN
มีขนาดใหญ่เช่นถ้าN * sizeof(elemType)
มากกว่าแคชขนาดแล้วทุกครั้งที่เข้าถึงsrc2[k][j]
จะหายไปจากแคช
มีวิธีการเพิ่มประสิทธิภาพที่หลากหลายสำหรับแคช นี่คือตัวอย่างง่ายๆ: แทนที่จะอ่านหนึ่งรายการต่อหนึ่งบรรทัดแคชในลูปด้านในให้ใช้รายการทั้งหมด:
int itemsPerCacheLine = CacheLineSize / sizeof(elemType);
for(i=0;i<N;i++) {
for(j=0;j<N;j += itemsPerCacheLine ) {
for(jj=0;jj<itemsPerCacheLine; jj+) {
dest[i][j+jj] = 0;
}
for( k=0;k<N;k++) {
for(jj=0;jj<itemsPerCacheLine; jj+) {
dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
}
}
}
}
หากขนาดของแคชบรรทัดคือ 64 ไบต์และเราดำเนินการกับ 32 บิต (4 ไบต์) ลอยตัวมี 16 รายการต่อหนึ่งบรรทัดแคช และจำนวนแคชที่หายไปจากการแปลงแบบเรียบง่ายนี้จะลดลงประมาณ 16 เท่า
การแปลงที่ชื่นชอบใช้งานได้บนไทล์ 2D เพิ่มประสิทธิภาพสำหรับแคชหลาย ๆ ตัว (L1, L2, TLB) และอื่น ๆ
ผลลัพธ์บางส่วนของ "การบล็อกแคช" ของ googling:
http://stumptown.cc.gt.atl.ga.us/cse6230-hpcta-fa11/slides/11a-matmul-goto.pdf
http://software.intel.com/en-us/articles/cache-blocking-techniques
ภาพเคลื่อนไหววิดีโอที่ยอดเยี่ยมของอัลกอริธึมการบล็อกแคชที่ดีที่สุด
http://www.youtube.com/watch?v=IFWgwGMMrh0
การปูกระเบื้องวนรอบนั้นสัมพันธ์กันอย่างมาก:
k==;
ฉันหวังว่านี่เป็นตัวพิมพ์ผิดหรือเปล่า?
โปรเซสเซอร์ในปัจจุบันทำงานกับพื้นที่หน่วยความจำแบบเรียงซ้อนหลายระดับ ดังนั้น CPU จะมีหน่วยความจำที่อยู่บนชิป CPU ของตัวเอง สามารถเข้าถึงหน่วยความจำนี้ได้อย่างรวดเร็ว มีระดับของแคชที่ต่างกันแต่ละการเข้าถึงช้าลง (และใหญ่กว่า) ถัดไปจนกว่าคุณจะไปที่หน่วยความจำระบบซึ่งไม่ได้อยู่บน CPU และค่อนข้างช้ากว่าการเข้าถึง
ตามเหตุผลชุดคำสั่งของ CPU คุณเพียงอ้างถึงที่อยู่หน่วยความจำในพื้นที่ที่อยู่เสมือนยักษ์ เมื่อคุณเข้าถึงที่อยู่หน่วยความจำเดียวซีพียูจะดึงข้อมูลออกมา ในสมัยก่อนมันจะดึงที่อยู่เพียงอันเดียว แต่วันนี้ซีพียูจะดึงหน่วยความจำจำนวนมากรอบบิตที่คุณขอและคัดลอกลงในแคช จะถือว่าหากคุณขอที่อยู่เฉพาะที่มีโอกาสสูงที่คุณจะขอที่อยู่ใกล้ ๆ ในไม่ช้า ตัวอย่างเช่นหากคุณกำลังคัดลอกบัฟเฟอร์คุณจะอ่านและเขียนจากที่อยู่ติดต่อกัน - หลังหนึ่งอยู่อีกอันหนึ่ง
ดังนั้นวันนี้เมื่อคุณดึงข้อมูลที่อยู่มันจะตรวจสอบแคชระดับแรกเพื่อดูว่าได้อ่านที่อยู่นั้นไปยังแคชแล้วหรือไม่ถ้ามันหาไม่พบนี่เป็นข้อผิดพลาดของแคชและต้องออกไปสู่ระดับถัดไปของ แคชเพื่อค้นหาจนกระทั่งในที่สุดจะต้องออกไปสู่หน่วยความจำหลัก
รหัสที่เป็นมิตรกับแคชพยายามที่จะทำให้การเข้าถึงใกล้กันในหน่วยความจำเพื่อลดการพลาดแคช
ตัวอย่างจะจินตนาการว่าคุณต้องการคัดลอกตาราง 2 มิติขนาดยักษ์ มันถูกจัดระเบียบด้วยการเข้าถึงแถวในหน่วยความจำติดต่อกันและหนึ่งแถวตามหลังถัดไปทันที
หากคุณคัดลอกองค์ประกอบหนึ่งครั้งจากซ้ายไปขวา - นั่นจะเป็นมิตรกับแคช หากคุณตัดสินใจที่จะคัดลอกตารางหนึ่งคอลัมน์ในแต่ละครั้งคุณจะคัดลอกจำนวนหน่วยความจำที่แน่นอน - แต่จะเป็นแคชที่ไม่เป็นมิตร
จะต้องมีการชี้แจงว่าข้อมูลไม่เพียง แต่ควรจะเป็นมิตรกับแคช แต่มันก็มีความสำคัญสำหรับรหัส นี่คือนอกเหนือไปจากการทำนายสาขา, การจัดเรียงคำสั่งใหม่, หลีกเลี่ยงการแบ่งที่เกิดขึ้นจริงและเทคนิคอื่น ๆ
โดยทั่วไปรหัสที่หนาแน่นขึ้นจะต้องใช้แคชในการเก็บน้อยลง ส่งผลให้มีข้อมูลแคชมากขึ้นสำหรับข้อมูล
รหัสไม่ควรเรียกใช้ฟังก์ชั่นทั่วสถานที่เพราะโดยทั่วไปแล้วจะต้องมีหนึ่งหรือหลายบรรทัดแคชของตัวเองส่งผลให้สายแคชน้อยลงสำหรับข้อมูล
ฟังก์ชั่นควรเริ่มต้นจากที่อยู่ที่เหมาะกับการจัดแนวแคช แม้ว่าจะมีสวิตช์คอมไพเลอร์ (gcc) สำหรับสิ่งนี้โปรดทราบว่าหากฟังก์ชั่นนั้นสั้นมากมันอาจจะสิ้นเปลืองสำหรับแต่ละคนที่จะครอบครองแคชไลน์ทั้งหมด ตัวอย่างเช่นหากฟังก์ชั่นที่ใช้บ่อยที่สุดสามฟังก์ชั่นอยู่ในแคชบรรทัด 64 ไบต์หนึ่งรายการนี่จะสิ้นเปลืองน้อยกว่าหากแต่ละคนมีบรรทัดของตัวเองและส่งผลให้แคชสองบรรทัดไม่พร้อมใช้งานสำหรับการใช้งานอื่น ค่าการจัดตำแหน่งทั่วไปอาจเป็น 32 หรือ 16
ดังนั้นใช้เวลาเพิ่มเพื่อทำให้รหัสมีความหนาแน่น ทดสอบโครงสร้างต่าง ๆ รวบรวมและตรวจสอบขนาดรหัสและโปรไฟล์ที่สร้างขึ้น
ดังที่ @Marc Claesen กล่าวถึงวิธีหนึ่งในการเขียนโค้ดที่เป็นมิตรกับแคชคือการใช้ประโยชน์จากโครงสร้างที่จัดเก็บข้อมูลของเรา นอกจากนั้นอีกวิธีหนึ่งในการเขียนรหัสที่เป็นมิตรกับแคชคือ: เปลี่ยนวิธีการจัดเก็บข้อมูลของเรา จากนั้นเขียนรหัสใหม่เพื่อเข้าถึงข้อมูลที่เก็บไว้ในโครงสร้างใหม่นี้
สิ่งนี้สมเหตุสมผลในกรณีที่ระบบฐานข้อมูลจัดวางตำแหน่งของตารางเป็นเส้นตรงและจัดเก็บ มีวิธีพื้นฐานสองวิธีในการจัดเก็บสิ่งอันดับของตารางเช่นที่เก็บแถวและที่เก็บคอลัมน์ ในการจัดเก็บแถวเป็นชื่อแนะนำ tuples จะถูกเก็บไว้แถวที่ชาญฉลาด สมมติว่าตารางProduct
ที่จัดเก็บชื่อนั้นมี 3 คุณลักษณะคือint32_t key, char name[56]
และint32_t price
ขนาดรวมของสิ่งอันดับคือ64
ไบต์
เราสามารถจำลองการประมวลผลเคียวรีร้านค้าแถวขั้นพื้นฐานในหน่วยความจำหลักโดยการสร้างอาร์เรย์ของProduct
struct ด้วยขนาด N โดยที่ N คือจำนวนแถวในตาราง เลย์เอาต์ของหน่วยความจำแบบนี้เรียกว่าอาเรย์ของ struct ดังนั้นโครงสร้างของผลิตภัณฑ์จึงเป็นเช่น:
struct Product
{
int32_t key;
char name[56];
int32_t price'
}
/* create an array of structs */
Product* table = new Product[N];
/* now load this array of structs, from a file etc. */
ในทำนองเดียวกันเราสามารถจำลองการประมวลผลแบบสอบถามในคอลัมน์พื้นฐานในหน่วยความจำหลักได้โดยสร้างอาร์เรย์ขนาด 3 อาร์เรย์ซึ่งเป็นหนึ่งอาร์เรย์สำหรับแต่ละแอตทริบิวต์ของProduct
ตาราง เลย์เอาต์ของหน่วยความจำดังกล่าวเรียกว่าโครงสร้างของอาร์เรย์ ดังนั้น 3 อาร์เรย์สำหรับแต่ละคุณลักษณะของผลิตภัณฑ์จึงเป็นเช่น:
/* create separate arrays for each attribute */
int32_t* key = new int32_t[N];
char* name = new char[56*N];
int32_t* price = new int32_t[N];
/* now load these arrays, from a file etc. */
ตอนนี้หลังจากโหลดทั้งอาร์เรย์ของ structs (Row Layout) และ 3 อาร์เรย์ที่แยกจากกัน (Layout Layout) เรามีที่เก็บแถวและที่เก็บคอลัมน์บนตารางของเรา Product
มีอยู่ในหน่วยความจำของเรา
ตอนนี้เราไปยังส่วนของรหัสที่เป็นมิตรกับแคช สมมติว่าเวิร์กโหลดในตารางของเรานั้นเป็นเช่นนั้นซึ่งเรามีคิวรี่การรวมในคุณลักษณะราคา เช่น
SELECT SUM(price)
FROM PRODUCT
สำหรับที่เก็บแถวเราสามารถแปลงแบบสอบถาม SQL ข้างต้นเป็น
int sum = 0;
for (int i=0; i<N; i++)
sum = sum + table[i].price;
สำหรับที่เก็บคอลัมน์เราสามารถแปลงแบบสอบถาม SQL ข้างต้นเป็น
int sum = 0;
for (int i=0; i<N; i++)
sum = sum + price[i];
รหัสสำหรับที่เก็บคอลัมน์จะเร็วกว่ารหัสสำหรับเค้าโครงแถวในแบบสอบถามนี้เนื่องจากต้องการเพียงชุดย่อยของแอตทริบิวต์และในเค้าโครงคอลัมน์ที่เรากำลังทำอยู่นั่นคือเข้าถึงเฉพาะคอลัมน์ราคาเท่านั้น
สมมติว่าขนาดบรรทัดแคชคือ 64
ไบต์
ในกรณีของเลย์เอาต์แถวเมื่ออ่านบรรทัดแคชมูลค่าราคาเพียง 1 (cacheline_size/product_struct_size = 64/64 = 1
) tuple นั้นถูกอ่านเนื่องจากขนาด struct ของเราที่ 64 ไบต์และเต็มบรรทัดแคชทั้งหมดของเราดังนั้นสำหรับแคช tuple ทุกครั้งที่เกิดขึ้นในกรณี ของเค้าโครงแถว
ในกรณีของการจัดวางคอลัมน์เมื่ออ่านบรรทัดแคชมูลค่าราคา 16 (cacheline_size/price_int_size = 64/4 = 16
) tuples จะถูกอ่านเนื่องจากค่าราคาต่อเนื่อง 16 ค่าที่เก็บไว้ในหน่วยความจำจะถูกนำเข้าไปในแคชดังนั้นสำหรับทุก ๆ สิบหกสิบอันดับ เค้าโครงคอลัมน์
ดังนั้นรูปแบบคอลัมน์จะเร็วขึ้นในกรณีของการสืบค้นที่กำหนดและเร็วกว่าในการรวมการสืบค้นในส่วนย่อยของคอลัมน์ของตาราง คุณสามารถลองทำการทดลองด้วยตัวคุณเองโดยใช้ข้อมูลจากเกณฑ์มาตรฐานTPC-Hและเปรียบเทียบเวลาทำงานของทั้งการจัดหน้า วิกิพีเดียบทความในคอลัมน์ที่มุ่งเน้นระบบฐานข้อมูลยังเป็นสิ่งที่ดี
ดังนั้นในระบบฐานข้อมูลหากเป็นที่รู้จักกันก่อนหน้านี้เราสามารถจัดเก็บข้อมูลของเราในรูปแบบที่เหมาะกับการสืบค้นในปริมาณงานและเข้าถึงข้อมูลจากเค้าโครงเหล่านี้ ในกรณีของตัวอย่างข้างต้นเราสร้างเค้าโครงคอลัมน์และเปลี่ยนรหัสของเราเพื่อคำนวณผลรวมเพื่อให้เป็นมิตรกับแคช
ระวังว่าแคชไม่เพียง แต่แคชหน่วยความจำต่อเนื่อง มีหลายบรรทัด (อย่างน้อย 4) ดังนั้นหน่วยความจำแบบไม่ต่อเนื่องและทับซ้อนจึงสามารถจัดเก็บได้อย่างมีประสิทธิภาพ
สิ่งที่ขาดหายไปจากตัวอย่างด้านบนทั้งหมดเป็นเกณฑ์มาตรฐานที่วัดได้ มีตำนานเกี่ยวกับการแสดงมากมาย ถ้าคุณไม่วัดคุณก็ไม่รู้ อย่าทำให้รหัสของคุณซับซ้อนหากคุณไม่ได้ทำการปรับปรุง