เมื่อใดถ้าเคยการคลายการวนซ้ำยังคงมีประโยชน์อยู่หรือไม่?


93

ฉันพยายามเพิ่มประสิทธิภาพโค้ดที่มีความสำคัญอย่างยิ่ง (อัลกอริธึมการเรียงลำดับอย่างรวดเร็วที่ถูกเรียกเป็นล้าน ๆ ล้านครั้งในการจำลองมอนติคาร์โล) โดยการคลายการวนซ้ำ นี่คือวงในที่ฉันพยายามเร่งความเร็ว:

// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}

ฉันพยายามยกเลิกการเล่นบางอย่างเช่น:

while(true) {
    if(myArray[++index1] < pivot) break;
    if(myArray[++index1] < pivot) break;
    // More unrolling
}


while(true) {
    if(pivot < myArray[--index2]) break;
    if(pivot < myArray[--index2]) break;
    // More unrolling
}

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


1
ฉันขอถามว่าทำไมคุณไม่ใช้รูทีน Quicksort ไลบรารีมาตรฐาน?
Peter Alexander

14
@Poita: เนื่องจากของฉันมีคุณสมบัติพิเศษบางอย่างที่ฉันต้องการสำหรับการคำนวณทางสถิติที่ฉันกำลังทำและได้รับการปรับแต่งอย่างมากสำหรับกรณีการใช้งานของฉันดังนั้นจึงมีความกว้างน้อยกว่า แต่เร็วกว่า lib มาตรฐาน ฉันใช้ภาษาการเขียนโปรแกรม D ซึ่งมีเครื่องมือเพิ่มประสิทธิภาพเส็งเคร็งแบบเก่าและสำหรับอาร์เรย์แบบสุ่มขนาดใหญ่ฉันยังคงเอาชนะการเรียงลำดับ C ++ STL ของ GCC ได้ 10-20%
dsimcha

คำตอบ:


122

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

ตัวอย่างง่ายๆ:

for (int i=0; i<n; i++)
{
  sum += data[i];
}

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

ในทางกลับกันรหัสนี้:

for (int i=0; i<n; i+=4)
{
  sum1 += data[i+0];
  sum2 += data[i+1];
  sum3 += data[i+2];
  sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

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


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

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

2
โปรดทราบว่าผลลัพธ์จะไม่เหมือนตัวเลขกับลูปเดิมเมื่อคำนวณผลรวมด้วยวิธีนี้
Barabas

การพึ่งพาแบบวนซ้ำเป็นหนึ่งรอบการเพิ่ม แกน OoO จะทำได้ดี การคลายตัวที่นี่อาจช่วยให้ SIMD ลอยตัวได้ แต่นั่นไม่เกี่ยวกับ OoO
Veedrac

2
@Nils: ไม่มาก; mainstream x86 OoO CPU ยังคงใกล้เคียงกับ Core2 / Nehalem / K10 การติดตามหลังจากแคชพลาดยังค่อนข้างน้อยการซ่อนเวลาแฝงของ FP ยังคงเป็นประโยชน์หลัก ในปี 2010 ซีพียูที่สามารถโหลดได้ 2 ครั้งต่อนาฬิกานั้นหายากกว่าด้วยซ้ำ (มีแค่ AMD เพราะ SnB ยังไม่เปิดตัว) ดังนั้นตัวสะสมหลายตัวจึงมีค่าน้อยกว่าสำหรับโค้ดจำนวนเต็มมากกว่าตอนนี้ (แน่นอนว่านี่เป็นโค้ดสเกลาร์ที่ควรทำให้เป็นเวกเตอร์อัตโนมัติ ดังนั้นใครจะรู้ว่าคอมไพเลอร์จะเปลี่ยนตัวสะสมหลายตัวให้เป็นองค์ประกอบเวกเตอร์หรือเป็นตัวสะสมเวกเตอร์หลายตัว... )
ปีเตอร์คอร์เดส

25

สิ่งเหล่านี้จะไม่สร้างความแตกต่างเพราะคุณทำการเปรียบเทียบจำนวนเท่ากัน นี่คือตัวอย่างที่ดีกว่า แทน:

for (int i=0; i<200; i++) {
  doStuff();
}

เขียน:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

ถึงแม้ว่ามันจะไม่สำคัญ แต่ตอนนี้คุณกำลังทำการเปรียบเทียบ 50 ครั้งแทนที่จะเป็น 200 (ลองนึกภาพการเปรียบเทียบนั้นซับซ้อนกว่า)

การคลายลูปแบบแมนนวลโดยทั่วไปส่วนใหญ่เป็นสิ่งประดิษฐ์ของประวัติศาสตร์อย่างไรก็ตาม เป็นอีกหนึ่งรายการที่เพิ่มขึ้นเรื่อย ๆ ซึ่งคอมไพเลอร์ที่ดีจะทำเพื่อคุณเมื่อมันสำคัญ ตัวอย่างเช่นคนส่วนใหญ่ไม่ได้รำคาญที่จะเขียนx <<= 1หรือแทนx += x x *= 2คุณเพียงแค่เขียนx *= 2และคอมไพเลอร์จะปรับให้เหมาะสมกับสิ่งที่ดีที่สุด

โดยทั่วไปแล้วความจำเป็นในการคาดเดาคอมไพเลอร์ของคุณจะน้อยลงมากขึ้น


1
@Mike ปิดการเพิ่มประสิทธิภาพอย่างแน่นอนหากเป็นความคิดที่ดีเมื่องงงวย แต่ก็ควรอ่านลิงก์ที่ Poita_ โพสต์ คอมไพเลอร์จะได้รับความเจ็บปวดที่ดีในธุรกิจที่
dmckee --- อดีตผู้ดูแลลูกแมว

16
@ ไมค์ "ฉันสามารถตัดสินใจได้อย่างสมบูรณ์แบบว่าจะไม่ทำสิ่งเหล่านั้นเมื่อไหร่หรือเมื่อไหร่" ... ฉันสงสัยนะเว้นแต่คุณจะเป็นยอดมนุษย์
คุณบอย

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

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

2
ประสิทธิภาพที่เพิ่มขึ้นเนื่องจากการคลายการวนซ้ำไม่เกี่ยวข้องกับการเปรียบเทียบที่คุณกำลังบันทึก ไม่มีไรเลย.
bobbogo

14

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

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

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


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

4
"การเพิ่มประสิทธิภาพด้วยมือแทบจะไม่มีประสิทธิภาพ" →อาจเป็นจริงหากคุณยังใหม่กับงานนี้ ไม่เป็นความจริงอย่างอื่น
Veedrac

ในปี 2019 ฉันยังคงทำการ unrolls ด้วยตนเองโดยได้รับผลประโยชน์มากมายจากความพยายามอัตโนมัติของคอมไพเลอร์ .. ดังนั้นจึงไม่น่าเชื่อถือที่จะปล่อยให้คอมไพเลอร์ทำทุกอย่าง ดูเหมือนว่าจะไม่คลายทั้งหมดนั้นบ่อยๆ อย่างน้อยสำหรับ c # ฉันไม่สามารถพูดในนามของทุกภาษา
WDUK

2

เท่าที่ฉันเข้าใจคอมไพเลอร์สมัยใหม่ได้คลายการวนลูปตามความเหมาะสมแล้วตัวอย่างเช่น gcc หากผ่านแฟล็กการปรับให้เหมาะสมตามที่คู่มือระบุว่าจะ:

คลายลูปซึ่งสามารถกำหนดจำนวนการวนซ้ำได้ในเวลาคอมไพล์หรือเมื่อเข้าสู่ลูป

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


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

2

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


ทำไมโดยเฉพาะอย่างยิ่งในการรับซีพียู x86
JohnTortugo

7
@JohnTortugo: ซีพียู x86 ที่ทันสมัยมีการเพิ่มประสิทธิภาพบางอย่างสำหรับลูปขนาดเล็ก - ดูเช่น Loop Stream Detector บนสถาปัตยกรรม Core และ Nehalem - การคลายการวนซ้ำเพื่อไม่ให้มีขนาดเล็กพอที่จะพอดีกับแคช LSD อีกต่อไปจะเอาชนะการเพิ่มประสิทธิภาพนี้ ดูเช่นtomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R

1

การพยายามโดยไม่รู้ตัวไม่ใช่หนทางที่จะทำได้
การเรียงลำดับนี้ใช้เวลาโดยรวมสูงหรือไม่

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

นี่คือตัวอย่างวิธีการรับประสิทธิภาพสูงสุด


1

การคลายการวนซ้ำอาจเป็นประโยชน์ในบางกรณี การได้รับเพียงอย่างเดียวไม่ได้ข้ามการทดสอบบางอย่าง!

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

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


0

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

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


ฉันกำลังคลายการเรียงลำดับอย่างรวดเร็วที่เรียกจากวงในของการจำลองของฉันไม่ใช่ลูปหลักของการจำลอง
dsimcha

0

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

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

การเปรียบเทียบ (ไปยังจุดสิ้นสุดของลูป) ก็เป็นข้อเสียเปรียบที่สำคัญเช่นกันหากการเปรียบเทียบมีน้ำหนักมาก (เช่นไม่ใช่testคำสั่ง) โดยเฉพาะอย่างยิ่งหากขึ้นอยู่กับฟังก์ชันภายนอก

การคลายการวนซ้ำช่วยเพิ่มการรับรู้ของ CPU สำหรับการทำนายสาขาเช่นกัน แต่สิ่งเหล่านี้ก็เกิดขึ้นอยู่ดี

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