หนึ่งเขียนรหัสที่ใช้แคช CPU ที่ดีที่สุดเพื่อปรับปรุงประสิทธิภาพได้อย่างไร


159

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

  1. วิธีการสร้างโค้ด, การแคชที่มีประสิทธิภาพ / การแคช (การแคชที่มากขึ้น, การทำแคชให้น้อยที่สุด) จากมุมมองทั้งแคชข้อมูลและแคชโปรแกรม (แคชคำสั่ง) คือสิ่งใดในรหัสที่เกี่ยวข้องกับโครงสร้างข้อมูลและการสร้างรหัสเราควรดูแลเพื่อให้แคชมีประสิทธิภาพ

  2. มีโครงสร้างข้อมูลใดที่หนึ่งต้องใช้ / หลีกเลี่ยงหรือมีวิธีการเข้าถึงสมาชิกของโครงสร้างนั้น ฯลฯ ... เพื่อให้แคชรหัสมีประสิทธิภาพ

  3. มีโปรแกรมใด ๆ ที่สร้าง (ถ้า, สำหรับ, สวิตช์, หยุดพัก, goto, ... ), code-flow (สำหรับภายในถ้า, ถ้าภายใน, สำหรับ, ฯลฯ ... ) หนึ่งควรปฏิบัติตาม / หลีกเลี่ยงในเรื่องนี้หรือไม่?

ฉันรอคอยที่จะได้รับประสบการณ์ของแต่ละบุคคลที่เกี่ยวข้องกับการสร้างรหัสประสิทธิภาพแคชโดยทั่วไป มันสามารถเป็นภาษาการเขียนโปรแกรมใด ๆ (C, C ++, Assembly, ... ), เป้าหมายฮาร์ดแวร์ใด ๆ (ARM, Intel, PowerPC, ... ), OS ใด ๆ (Windows, Linux, S ymbian, ... ) ฯลฯ .

ความหลากหลายจะช่วยให้เข้าใจได้ลึกซึ้งยิ่งขึ้น


1
ในฐานะที่เป็นคำนำของการพูดคุยนี้ให้ภาพรวมที่ดีyoutu.be/BP6NxVxDQIs
schoetbi

ดูเหมือนว่า URL ย่อที่สั้นกว่าไม่สามารถใช้งานได้อีกต่อไปนี่เป็น URL แบบเต็มสำหรับการพูดคุย: youtube.com/watch?v=BP6NxVxDQIs
Abhinav Upadhyay

คำตอบ:


119

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

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

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

การปรับปรุงการใช้งานแคชบรรทัดช่วยในสามประการ:

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

เทคนิคทั่วไปคือ:

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

เราควรทราบด้วยว่ามีวิธีอื่นในการซ่อนเวลาแฝงของหน่วยความจำมากกว่าการใช้แคช

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

การจัดกลุ่มคำแนะนำในลักษณะที่ว่าผู้ที่มักจะพลาดในแคชเกิดขึ้นใกล้กับแต่ละอื่น ๆ , CPU สามารถบางครั้งซ้อนทับกันการดึงข้อมูลเหล่านี้เพื่อให้แอพลิเคชันเพียงรักษาหนึ่งแฝงตี ( หน่วยความจำระดับขนาน )

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

การผสานลูปที่สัมผัสกับข้อมูลเดียวกัน (การรวมแบบวนซ้ำ ) และใช้เทคนิคการเขียนใหม่ที่รู้จักกันในชื่อการเรียงต่อกันหรือการบล็อกทั้งหมดพยายามหลีกเลี่ยงการดึงหน่วยความจำเพิ่มเติม

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

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


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

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

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

1
@javapowered คอมไพเลอร์อาจจะทำเช่นนั้นขึ้นอยู่กับภาษาแม้ว่าฉันจะไม่แน่ใจว่าพวกเขาทำอะไร เหตุผลที่คุณไม่สามารถทำได้ใน C คือมันใช้ได้อย่างสมบูรณ์กับที่อยู่ของสมาชิกตามที่อยู่ฐาน + ออฟเซ็ตมากกว่าตามชื่อซึ่งหมายความว่าการเรียงลำดับสมาชิกใหม่จะทำให้โปรแกรมแตก
Dan Bechard

56

ฉันไม่อยากจะเชื่อเลยว่าจะไม่มีคำตอบสำหรับสิ่งนี้อีกแล้ว อย่างไรก็ตามตัวอย่างคลาสสิกอย่างหนึ่งคือการย้ำอาร์เรย์หลายมิติ "Inside Out":

pseudocode
for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[j][i]

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

for (i = 0 to size)
  for (j = 0 to size)
    do something with ary[i][j]

มันจะทำงานได้เร็วขึ้นมาก


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

11
อันที่จริงคอมไพเลอร์สมัยใหม่ส่วนใหญ่สามารถเข้าใจสิ่งนี้ได้ด้วยตัวของมันเอง (เมื่อเปิดใช้การปรับให้เหมาะสมที่สุด)
Ricardo Nolde

1
@ykaganovich นั่นเป็นตัวอย่างใน Ulrich Dreppers บทความ: lwn.net/Articles/255364
Simon Stender Boisen

ฉันไม่แน่ใจว่าสิ่งนี้ถูกต้องเสมอ - หากทั้งอาร์เรย์พอดีภายในแคช L1 (มักเป็น 32k!) คำสั่งซื้อทั้งสองจะมีจำนวนการเข้าชมแคชและการพลาดที่เท่ากัน บางทีการดึงข้อมูลล่วงหน้าของหน่วยความจำอาจมีผลกระทบบ้างฉันเดา ยินดีที่ได้รับการแก้ไขแน่นอน
Matt Parkins

ใครจะเป็นผู้เลือกรุ่นแรกของรหัสนี้หากการสั่งซื้อไม่สำคัญ
silver_rocket

45

จริงๆแล้วกฎพื้นฐานนั้นค่อนข้างง่าย จุดที่มันยุ่งยากในการที่จะใช้กับโค้ดของคุณได้อย่างไร

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

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

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

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

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

การใช้คำสั่งแคชมักจะมีปัญหาน้อยกว่า สิ่งที่คุณต้องกังวลคือ data cache

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

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


4
+1 คำแนะนำที่ดีและเป็นประโยชน์ สิ่งที่เพิ่มเติม: การรวมเวลาท้องถิ่นและพื้นที่ท้องถิ่นรวมกันแนะนำว่าสำหรับเมทริกซ์ ops มันอาจจะแนะนำให้แบ่งพวกเขาออกเป็นเมทริกซ์ขนาดเล็กที่สมบูรณ์ในบรรทัดแคชหรือแถว / คอลัมน์ที่เหมาะสมในบรรทัดแคช ฉันจำได้ว่าทำเช่นนั้นเพื่อการสร้างภาพของ multidim ข้อมูล. มันให้เตะอย่างจริงจังในกางเกง มันเป็นการดีที่จะจำไว้ว่าแคชเก็บมากกว่าหนึ่งบรรทัด ';)
AndreasT

1
คุณบอกว่ามีเพียง 1 CPU เท่านั้นที่สามารถมีที่อยู่ที่กำหนดในแคช L1 ในเวลา - ฉันถือว่าคุณหมายถึงสายแคชมากกว่าที่อยู่ ฉันเคยได้ยินปัญหาการแบ่งปันที่ผิดพลาดเมื่อซีพียูอย่างน้อยหนึ่งทำการเขียน แต่ไม่ใช่ถ้าทั้งคู่กำลังอ่านเท่านั้น ดังนั้นโดย 'เข้าถึง' คุณหมายถึงการเขียนจริงเหรอ?
โจเซฟการ์วิน

2
@JosephGarvin: ใช่ฉันหมายถึงการเขียน คุณถูกต้องหลายแกนสามารถมีสายแคชเดียวกันในแคช L1 ของพวกเขาในเวลาเดียวกัน แต่เมื่อแกนหลักหนึ่งเขียนไปยังที่อยู่เหล่านี้มันจะได้รับการทำให้ใช้งานไม่ได้ในแคช L1 อื่น ๆ ทั้งหมดแล้วพวกเขาจะต้องโหลดซ้ำก่อนที่จะทำได้ อะไรกับมัน ขออภัยที่ใช้ถ้อยคำที่ไม่ถูกต้อง :)
jalf

44

ฉันขอแนะนำให้อ่านบทความ 9 ส่วนสิ่งที่โปรแกรมเมอร์ทุกคนควรรู้เกี่ยวกับหน่วยความจำโดย Ulrich Drepper หากคุณสนใจว่าหน่วยความจำและซอฟต์แวร์โต้ตอบกันอย่างไร นอกจากนี้ยังสามารถใช้ได้เป็น104 หน้าไฟล์ PDF

ส่วนที่เกี่ยวข้องกับคำถามนี้อาจเป็นส่วนที่ 2 (แคช CPU) และส่วนที่ 5 (โปรแกรมเมอร์สามารถทำอะไร - การเพิ่มประสิทธิภาพแคช)


16
คุณควรเพิ่มบทสรุปของประเด็นหลักจากบทความ
Azmisov

ยอดเยี่ยมอ่าน แต่หนังสืออีกเล่มที่ต้องกล่าวถึงที่นี่คือHennessy, Patterson, สถาปัตยกรรมคอมพิวเตอร์, A Quantitiative Approachซึ่งมีให้บริการในรุ่นที่ 5 ภายในวันนี้
Haymo Kutschbach

15

นอกเหนือจากรูปแบบการเข้าถึงข้อมูลเป็นปัจจัยสำคัญในรหัสแคชง่ายคือข้อมูลขนาด ข้อมูลที่น้อยลงหมายถึงมันเหมาะสมกับแคชมากขึ้น

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

ในทำนองเดียวกันอาร์เรย์ Java บูลีนใช้ทั้งไบต์สำหรับแต่ละค่าเพื่อให้การดำเนินงานในแต่ละค่าโดยตรง คุณสามารถลดขนาดข้อมูลลงได้ 8 หากคุณใช้บิตจริง แต่จากนั้นการเข้าถึงค่าแต่ละค่าจะซับซ้อนมากขึ้นซึ่งต้องใช้การดำเนินการบิตกะและมาสก์ ( BitSetคลาสจะช่วยคุณได้) อย่างไรก็ตามเนื่องจากเอฟเฟ็กต์แคชอาจยังเร็วกว่าการใช้บูลีน [] เมื่ออาร์เรย์มีขนาดใหญ่ IIRC ฉันเคยเร่งความเร็วด้วย 2 หรือ 3 ด้วยวิธีนี้


9

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

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

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

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


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

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

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

8

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

ดังนั้นในช่วงลูปฟิสิกส์ข้อมูลฟิสิกส์จะถูกประมวลผลตามลำดับอาร์เรย์โดยใช้เวกเตอร์คณิตศาสตร์ วัตถุของเกมใช้ ID วัตถุเป็นดัชนีในอาร์เรย์ต่างๆ มันไม่ได้เป็นตัวชี้เพราะพอยน์เตอร์อาจกลายเป็นโมฆะถ้าจำเป็นต้องย้ายอาร์เรย์

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

ตัวอย่างนี้อาจล้าสมัยเพราะฉันคาดหวังว่าเกมที่ทันสมัยส่วนใหญ่จะใช้เอนจิ้นฟิสิกส์ที่สร้างไว้ล่วงหน้าอย่าง Havok


2
+1 ไม่ล้าสมัย นี่เป็นวิธีที่ดีที่สุดในการจัดระเบียบข้อมูลสำหรับเอ็นจิ้นเกม - สร้างบล็อคข้อมูลต่อเนื่องและดำเนินการตามประเภทที่กำหนด (พูด AI) ก่อนที่จะย้ายไปยังถัดไป (พูดฟิสิกส์) เพื่อใช้ประโยชน์จากความใกล้ชิดแคช / ท้องถิ่น การอ้างอิง
วิศวกร

ฉันเห็นตัวอย่างที่แน่นอนนี้ในวิดีโอเมื่อสองสามสัปดาห์ที่ผ่านมา แต่เมื่อสูญเสียลิงก์ไปแล้ว / จำไม่ได้ว่าจะหาได้อย่างไร อย่าลืมที่คุณเห็นตัวอย่างนี้?
จะ

@ จะไม่: ฉันจำไม่ได้ว่าอยู่ที่ไหน
Zan Lynx

นี่เป็นแนวคิดของระบบส่วนประกอบของเอนทิตี (ECS: en.wikipedia.org/wiki/Entity_component_system ) เก็บข้อมูลเป็น struct-of-arrays แทน array-of-structs แบบดั้งเดิมที่ OOP ให้การสนับสนุน
BuschnicK

7

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


7

ข้อความถึง "ตัวอย่างคลาสสิก" โดยผู้ใช้1800 ข้อมูล (ยาวเกินไปสำหรับความคิดเห็น)

ฉันต้องการตรวจสอบความแตกต่างของเวลาสำหรับคำสั่งวนซ้ำสองคำ ("outter" และ "inner") ดังนั้นฉันจึงทำการทดลองอย่างง่าย ๆ ด้วยอาร์เรย์ 2D ขนาดใหญ่:

measure::start();
for ( int y = 0; y < N; ++y )
for ( int x = 0; x < N; ++x )
    sum += A[ x + y*N ];
measure::stop();

และกรณีที่สองที่มีการforสลับลูป

รุ่นที่ช้ากว่า ("x แรก") คือ 0.88 วินาทีและรุ่นที่เร็วกว่าคือ 0.06 วินาที นั่นคือพลังของการแคช :)

ฉันใช้gcc -O2และยังคงลูปไม่ได้รับการปรับให้เหมาะสม ความคิดเห็นของริคาร์โด้ว่า "คอมไพเลอร์สมัยใหม่ส่วนใหญ่สามารถเข้าใจสิ่งนี้โดยตัวของมันเอง" ไม่ได้ถือ


ไม่แน่ใจว่าฉันได้รับสิ่งนี้ ในทั้งสองตัวอย่างคุณยังคงเข้าถึงตัวแปรแต่ละตัวใน for for loop เหตุใดจึงเร็วกว่าอีกวิธีหนึ่ง
ed-

ในที่สุดใช้งานง่ายสำหรับผมที่จะเข้าใจว่ามันมีผล :)
Laie

@EdwardCorlew เป็นเพราะลำดับที่เข้าถึงได้ คำสั่ง y-first นั้นเร็วกว่าเพราะเข้าถึงข้อมูลตามลำดับ เมื่อรายการแรกถูกร้องขอแคช L1 จะโหลดทั้งแคชไลน์ซึ่งรวมถึง int ที่ร้องขอบวก 15 ถัดไป (สมมติว่าแคชไบต์ 64- ไบต์) ดังนั้นจึงไม่มี CPU แผงลอยรอ 15 หน้าถัดไป x - ลำดับแรกจะช้ากว่าเนื่องจากองค์ประกอบที่เข้าถึงไม่ได้เรียงตามลำดับและสันนิษฐานว่า N มีขนาดใหญ่พอที่หน่วยความจำที่เข้าถึงได้จะอยู่นอกแคช L1 เสมอและทุกครั้งที่แผงการทำงานหยุดทำงาน
Matt Parkins

4

ฉันสามารถตอบ (2) โดยบอกว่าในโลก C ++ รายการที่เชื่อมโยงสามารถฆ่าแคช CPU ได้อย่างง่ายดาย อาร์เรย์เป็นทางออกที่ดีกว่าถ้าเป็นไปได้ ไม่มีประสบการณ์ว่าจะใช้กับภาษาอื่นหรือไม่ แต่เป็นเรื่องง่ายที่จะจินตนาการว่าปัญหาเดียวกันจะเกิดขึ้น


@Andrew: วิธีการเกี่ยวกับโครงสร้าง แคชมีประสิทธิภาพหรือไม่ พวกเขามีข้อ จำกัด ขนาดใด ๆ ที่จะแคชมีประสิทธิภาพ?
goldenmean

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

วิธีใช้รายการที่เชื่อมโยงโดยไม่ฆ่าแคชซึ่งมีประสิทธิภาพมากที่สุดสำหรับรายการที่ไม่ใหญ่คือการสร้างพูลหน่วยความจำของคุณเองนั่นคือ - เพื่อจัดสรรอาร์เรย์ขนาดใหญ่หนึ่งชุด จากนั้นแทนที่จะเป็น 'malloc'ing (หรือ' new'ing ใน C ++) หน่วยความจำสำหรับสมาชิกลิสต์รายเล็กที่เชื่อมโยงกันซึ่งอาจถูกจัดสรรในตำแหน่งที่แตกต่างกันอย่างสิ้นเชิงในหน่วยความจำและเสียพื้นที่จัดการคุณให้หน่วยความจำจากพูลหน่วยความจำ ราคาที่เพิ่มขึ้นอย่างมากที่ปิดสมาชิกอย่างมีเหตุผลของรายการจะอยู่ในแคชด้วยกัน
Liran Orevi

แน่นอน แต่มันเป็นงานจำนวนมากที่ได้รับ std :: list <> et al เพื่อใช้บล็อกหน่วยความจำที่กำหนดเองของคุณ เมื่อตอนที่ฉันยังเป็นเด็กเล็กคนหนึ่งฉันจะไปทางนั้นอย่างแน่นอน แต่วันนี้ ... สิ่งอื่น ๆ อีกมากมายเกินกว่าที่จะแก้ไขได้
แอนดรู


4

แคชถูกจัดเรียงใน "แคชไลน์" และหน่วยความจำ (จริง) ถูกอ่านและเขียนเป็นหน่วยขนาดนี้

โครงสร้างข้อมูลที่มีอยู่ภายในแคชบรรทัดเดียวจึงมีประสิทธิภาพมากกว่า

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

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


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

4

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

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

(btw - ไม่ดีเท่าที่แคชแคชอาจมีอยู่แย่กว่านั้น - หากโปรแกรมแบ่งหน้าจากฮาร์ดไดรฟ์ ... )


4

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

แนวคิดนี้ได้มาจากเทคนิคการวางท่ออื่น ๆ เช่นการวางท่อส่งคำสั่ง CPU

รูปแบบประเภทนี้เหมาะสมที่สุดกับขั้นตอนที่

  1. อาจถูกแบ่งย่อยลงเป็นหลายขั้นตอนย่อยอย่างสมเหตุสมผล S [1], S [2], S [3], ... ซึ่งเวลาดำเนินการนั้นเทียบเคียงได้กับเวลาเข้าถึง RAM (~ 60-70ns)
  2. ใช้เวลาชุดของการป้อนข้อมูลและทำหลายขั้นตอนดังกล่าวข้างต้นเพื่อให้ได้ผลลัพธ์

ลองทำกรณีง่าย ๆ ที่มีขั้นตอนย่อยเดียว โดยปกติแล้วรหัสต้องการ:

def proc(input):
    return sub-step(input))

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

def batch_proc(inputs):
    results = []
    for i in inputs:
        // avoids code cache miss, but still suffer data(inputs) miss
        results.append(sub-step(i))
    return res

อย่างไรก็ตามตามที่ได้กล่าวไว้ก่อนหน้านี้หากการดำเนินการตามขั้นตอนนั้นเหมือนกับเวลาเข้าถึง RAM คุณสามารถปรับปรุงโค้ดให้เป็นดังนี้:

def batch_pipelined_proc(inputs):
    for i in range(0, len(inputs)-1):
        prefetch(inputs[i+1])
        # work on current item while [i+1] is flying back from RAM
        results.append(sub-step(inputs[i-1]))

    results.append(sub-step(inputs[-1]))

การไหลของการดำเนินการจะมีลักษณะดังนี้:

  1. prefetch (1) ขอให้ CPU ป้อนข้อมูลล่วงหน้า [1] ลงในแคชโดยคำสั่ง prefetch นั้นใช้วงจร P รอบตัวเองและกลับมาและในอินพุตพื้นหลัง [1] จะมาถึงแคชหลังจากรอบ R
  2. works_on (0) Cold miss on 0 และใช้งานได้ซึ่งใช้ M
  3. prefetch (2) เรียกการดึงข้อมูลอื่น
  4. works_on (1) ถ้า P + R <= M ดังนั้นอินพุต [1] ควรอยู่ในแคชแล้วก่อนขั้นตอนนี้จึงหลีกเลี่ยงการแคชข้อมูล
  5. works_on (2) ...

อาจมีขั้นตอนเพิ่มเติมที่เกี่ยวข้องจากนั้นคุณสามารถออกแบบไปป์ไลน์แบบหลายขั้นตอนตราบใดที่จังหวะของขั้นตอนและการเข้าถึงหน่วยความจำแฝงตรงกันคุณจะประสบกับรหัส / แคชข้อมูลน้อย อย่างไรก็ตามกระบวนการนี้ต้องได้รับการปรับแต่งด้วยการทดลองจำนวนมากเพื่อค้นหาการจัดกลุ่มขั้นตอนและเวลาดึงข้อมูลล่วงหน้าที่ถูกต้อง เนื่องจากต้องการความพยายามจึงเห็นการยอมรับมากขึ้นในการประมวลผลข้อมูล / แพ็กเก็ตสตรีมประสิทธิภาพสูง ตัวอย่างรหัสการผลิตที่ดีสามารถพบได้ในการออกแบบท่อส่งก๊าซ DPDK QoS: http://dpdk.org/doc/guides/prog_guide/qos_framework.htmlบทที่ 21.2.4.3 ท่อส่งก๊าซ

ข้อมูลเพิ่มเติมสามารถพบได้:

https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and

http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf


1

เขียนโปรแกรมของคุณให้มีขนาดเล็กที่สุด นี่คือเหตุผลที่ไม่ควรใช้ -O3 optimisations สำหรับ GCC มันใช้ขนาดที่ใหญ่ขึ้น บ่อยครั้งที่ -Os นั้นดีพอ ๆ กับ -O2 ทุกอย่างขึ้นอยู่กับโปรเซสเซอร์ที่ใช้งาน YMMV

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

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

for(i = 0; i < MAX; ++i)
for(i = MAX; i > 0; --i)

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


จุดที่น่าสนใจ แคชที่มองไปข้างหน้าสร้างสมมติฐานตามทิศทางของลูป / ส่งผ่านหน่วยความจำหรือไม่?
Andrew

1
มีหลายวิธีในการออกแบบแคชข้อมูลเก็งกำไร คนที่ก้าวย่างจะวัด 'ระยะทาง' และ 'ทิศทาง' ของการเข้าถึงข้อมูล การไล่ล่าตัวชี้ที่อิงเนื้อหา มีวิธีอื่นในการออกแบบ
sybreon

1

นอกจากการจัดโครงสร้างและเขตข้อมูลของคุณแล้วหากโครงสร้างของคุณหากมีการจัดสรรฮีปคุณอาจต้องการใช้ตัวจัดสรรที่รองรับการจัดสรรแบบจัดชิด ชอบ _aligned_malloc (ขนาดของ (DATA), SYSTEM_CACHE_LINE_SIZE); มิฉะนั้นคุณอาจมีการแบ่งปันที่ผิดแบบสุ่ม โปรดจำไว้ว่าใน Windows ฮีปเริ่มต้นมีการจัดตำแหน่ง 16 ไบต์

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