มันเร็วกว่าที่จะนับถอยหลังหรือไม่?


131

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

for (i = N; i >= 0; i--)  
  putchar('*');  

ดีกว่า:

for (i = 0; i < N; i++)  
  putchar('*');  

มันเป็นเรื่องจริงเหรอ? และถ้าเป็นเช่นนั้นมีใครรู้ว่าทำไม?


6
นักคอมพิวเตอร์คนไหน ในสิ่งพิมพ์ใด
bmargulies

26
เป็นไปได้ว่าคุณสามารถประหยัดได้หนึ่งนาโนวินาทีต่อการทำซ้ำหรือประมาณเท่าผมหนึ่งเส้นในครอบครัวของแมมมอ ธ ที่มีขนแกะ putcharใช้ 99.9999% ของเวลา (ให้หรือใช้)
Mike Dunlavey

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

61
ลูปแรกช้าลงอย่างเห็นได้ชัดเนื่องจากมันเรียกพัตชาร์ 11 ครั้งในขณะที่วงที่สองเรียกมันว่า 10 ครั้งเท่านั้น
Paul Kuliniewicz

17
คุณสังเกตไหมว่าถ้าiไม่ได้ลงนามลูปแรกจะเป็นวงที่ไม่มีที่สิ้นสุด?
Shahbaz

คำตอบ:


372

มันเป็นเรื่องจริงเหรอ? และถ้าเป็นเช่นนั้นใครจะรู้ว่าทำไม?

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

ดังนั้นหากคุณยึดห่วงของคุณเพื่อเปรียบเทียบกับศูนย์แทนNสิ่งที่อาจเกิดขึ้น?

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

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

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


3
++ และนอกจากนี้putcharคำสั่งขนาดยาวกว่าลูปโอเวอร์เฮด
Mike Dunlavey

42
ไม่ใช่ตำนานอย่างเคร่งครัด: ถ้าเขากำลังทำระบบเรียลไทม์ที่ปรับให้เหมาะสมกับ uber มันจะมีประโยชน์ แต่แฮ็กเกอร์ประเภทนั้นอาจจะรู้เรื่องทั้งหมดนี้อยู่แล้วและแน่นอนว่าจะไม่ทำให้นักเรียน CS ระดับเริ่มต้นสับสนกับ arcana
Paul Nathan

4
@ โจชัว: การเพิ่มประสิทธิภาพนี้จะตรวจพบได้อย่างไร? ดังที่ผู้ถามกล่าวไว้ว่าดัชนีการวนซ้ำจะไม่ถูกใช้ในลูปดังนั้นหากจำนวนการวนซ้ำเท่าเดิมจะไม่มีการเปลี่ยนแปลงพฤติกรรม ในแง่ของการพิสูจน์ความถูกต้องการแทนที่ตัวแปรj=N-iแสดงให้เห็นว่าทั้งสองลูปมีค่าเท่ากัน
เพลงสดุดี

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

3
ฉันไม่รู้ว่าควรโหวตให้เนื้อหาหรือโหวตให้คะแนนสรุป
Danubian Sailor

29

นี่คือสิ่งที่อาจเกิดขึ้นกับฮาร์ดแวร์บางตัวขึ้นอยู่กับสิ่งที่คอมไพเลอร์สามารถอนุมานเกี่ยวกับช่วงของตัวเลขที่คุณใช้: ด้วยการวนซ้ำที่เพิ่มขึ้นคุณต้องทดสอบ i<Nทุกครั้งที่วนรอบ สำหรับรุ่น decrementing ธงพกพา (ตั้งค่าเป็นผลข้างเคียงของการลบ) i>=0อาจบอกคุณโดยอัตโนมัติหาก ซึ่งจะบันทึกการทดสอบต่อครั้งในการวนรอบ

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


2
นั่นจะไม่ใช่แฟล็กศูนย์และไม่ใช่แฟล็กพกพา?
Bob

2
@Bob ในกรณีนี้คุณอาจต้องการถึงศูนย์พิมพ์ผลลัพธ์ลดลงเพิ่มเติมจากนั้นพบว่าคุณมีค่าต่ำกว่าศูนย์ทำให้เกิดการพกพา (หรือยืม) แต่เขียนแตกต่างกันเล็กน้อยลูปลดลงอาจใช้แฟล็กศูนย์แทน
sigfpe

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

@Paul เนื่องจากฉันมีประสบการณ์กับ Atmel AVRs ฉันไม่ลืมที่จะพูดถึงไมโครคอนโทรลเลอร์ ...
sigfpe

27

ในชุดคำสั่ง Intel x86 การสร้างลูปเพื่อนับถอยหลังเป็นศูนย์โดยปกติสามารถทำได้โดยมีคำสั่งน้อยกว่าการวนซ้ำที่นับถึงเงื่อนไขการออกที่ไม่ใช่ศูนย์ โดยเฉพาะอย่างยิ่งการลงทะเบียน ECX มักใช้เป็นตัวนับลูปใน x86 asm และชุดคำสั่งของ Intel มีคำสั่ง jcxz jump แบบพิเศษที่ทดสอบการลงทะเบียน ECX สำหรับศูนย์และกระโดดตามผลการทดสอบ

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

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


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

1
@Mark: คอมไพเลอร์ Delphi เช่นกันเริ่มในปี 2539
dthorpe

4
@MarkRansom จริงๆแล้วคอมไพลเลอร์อาจสามารถใช้การวนซ้ำโดยใช้การนับถอยหลังแม้ว่าจะใช้ตัวแปรดัชนีลูปก็ตามทั้งนี้ขึ้นอยู่กับวิธีการใช้ในลูป หากใช้ตัวแปรดัชนีลูปเพื่อจัดทำดัชนีในอาร์เรย์แบบคงที่เท่านั้น (อาร์เรย์ของขนาดที่ทราบในเวลาคอมไพล์) การจัดทำดัชนีอาร์เรย์สามารถทำได้ในรูปแบบ ptr + ขนาดอาร์เรย์ - loop index var ซึ่งยังคงเป็นคำสั่งเดียวใน x86 ได้ มันค่อนข้างป่าที่จะดีบักแอสเซมเบลอร์และเห็นลูปกำลังนับถอยหลัง แต่ดัชนีอาร์เรย์จะเพิ่มขึ้น!
dthorpe

1
จริงๆแล้ววันนี้คอมไพเลอร์ของคุณอาจจะไม่ใช้คำสั่ง loop และ jecxz เนื่องจากช้ากว่าคู่ dec / jnz
fuz

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

23

ใช่..!!

การนับจาก N ถึง 0 จะเร็วกว่าเล็กน้อยซึ่งการนับจาก 0 ถึง N ในแง่ของฮาร์ดแวร์จะจัดการกับการเปรียบเทียบได้อย่างไร ..

สังเกตการเปรียบเทียบในแต่ละลูป

i>=0
i<N

โปรเซสเซอร์ส่วนใหญ่มีการเปรียบเทียบกับคำสั่งเป็นศูนย์ดังนั้นตัวแรกจะถูกแปลเป็นรหัสเครื่องเป็น:

  1. โหลด i
  2. เปรียบเทียบและกระโดดถ้าน้อยกว่าหรือเท่ากับศูนย์

แต่อันที่สองต้องโหลด N form Memory ทุกครั้ง

  1. โหลด i
  2. โหลด N
  3. Sub i และ N
  4. เปรียบเทียบและกระโดดถ้าน้อยกว่าหรือเท่ากับศูนย์

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

ดังนั้นการนับจาก 10 ถึง 100 จึงเหมือนกับการนับแบบ 100 ถึง 10
แต่การนับจาก i = 100 ถึง 0 จะเร็วกว่าจาก i = 0 ถึง 100 - ในกรณีส่วนใหญ่
และการนับจาก i = N ถึง 0 จะเร็วกว่าจาก i = 0 ถึง N

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

ที่เกี่ยวข้อง: เหตุใด n ++ จึงทำงานเร็วกว่า n = n + 1


6
ดังนั้นสิ่งที่คุณพูดคือการนับถอยหลังไม่ได้เร็วกว่าการเปรียบเทียบกับศูนย์เร็วกว่าค่าอื่น ๆ หมายถึงการนับ 10 ถึง 100 และการนับถอยหลังจาก 100 ถึง 10 จะเท่ากันหรือไม่?
Bob

8
ใช่ .. ไม่ใช่เรื่องของการ "นับถอยหลังหรือขึ้น" .. แต่เป็นเรื่องของการ "เปรียบเทียบกับอะไร" ..
Betamoo

3
ในขณะนี้เป็นจริงระดับแอสเซมเบลอร์ สองสิ่งที่รวมเข้ากับ meke ไม่เป็นความจริง - ฮาร์ดแวร์สมัยใหม่ที่ใช้ท่อยาวและคำแนะนำในการเก็งกำไรจะแอบอยู่ใน "Sub i และ N" โดยไม่เกิดวงจรเพิ่มเติม - และ - แม้แต่คอมไพเลอร์ที่โหดที่สุดก็จะปรับ "Sub i และ N "ออกจากการดำรงอยู่
James Anderson

2
@nico ไม่จำเป็นต้องเป็นระบบโบราณ มันจะต้องเป็นชุดคำสั่งที่มีการเปรียบเทียบกับการดำเนินการเป็นศูนย์ซึ่งเร็วกว่า / ดีกว่าการเปรียบเทียบกับค่ารีจิสเตอร์ x86 มีใน jcxz x64 ยังมีอยู่ ไม่โบราณ. นอกจากนี้สถาปัตยกรรม RISC มักเป็นกรณีพิเศษเป็นศูนย์ ตัวอย่างเช่นชิป DEC AXP Alpha (ในตระกูล MIPS) มี "การลงทะเบียนเป็นศูนย์" - อ่านเป็นศูนย์ไม่ต้องเขียนอะไรเลย การเปรียบเทียบกับการลงทะเบียนศูนย์แทนที่จะเทียบกับการลงทะเบียนทั่วไปที่มีค่าศูนย์จะช่วยลดการอ้างอิงระหว่างคำสั่งและช่วยในการดำเนินการตามคำสั่ง
dthorpe

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

12

ใน C ถึง psudo-assembly:

for (i = 0; i < 10; i++) {
    foo(i);
}

กลายเป็น

    clear i
top_of_loop:
    call foo
    increment i
    compare 10, i
    jump_less top_of_loop

ในขณะที่:

for (i = 10; i >= 0; i--) {
    foo(i);
}

กลายเป็น

    load i, 10
top_of_loop:
    call foo
    decrement i
    jump_not_neg top_of_loop

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

x = x - 0

มีความหมายเหมือนกับ

compare x, 0

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

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


เป็นไปได้ไหมว่ามี 2 คำสั่งต่างกันแทนที่จะเป็น 1 เท่านั้น?
Pacerier

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

6

นับถอยหลังเร็วขึ้นในกรณีเช่นนี้:

for (i = someObject.getAllObjects.size(); i >= 0; i--) {…}

เพราะsomeObject.getAllObjects.size()ดำเนินการครั้งเดียวในตอนเริ่มต้น


แน่นอนว่าพฤติกรรมที่คล้ายกันสามารถทำได้โดยการเรียกsize()ออกจากวงดังที่ปีเตอร์กล่าวถึง:

size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}

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

3
@Peter: เฉพาะในกรณีที่คอมไพลเลอร์รู้ว่าขนาด () บางอย่างเป็น idempotent ในลูปเท่านั้น นั่นอาจไม่ใช่กรณีเกือบตลอดเวลาเว้นแต่การวนซ้ำนั้นง่ายมาก
Lawrence Dol

@LawrenceDol, คอมไพเลอร์แน่นอนจะรู้ว่ามันจนกว่าคุณจะมีรหัส compilatino execแบบไดนามิกโดยใช้
Pacerier

4

นับถอยหลังเร็วกว่าขึ้นไหม

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

หากลูปกำลังทำงานมันจะขึ้นผ่านอาร์เรย์ (หรือรายการหรืออะไรก็ตาม) ตัวนับที่เพิ่มขึ้นมักจะจับคู่ได้ดีกว่ากับวิธีที่ผู้อ่านคิดว่าลูปกำลังทำอะไรอยู่ - เขียนโค้ดลูปด้วยวิธีนี้

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

รายละเอียดเพิ่มเติมเล็กน้อยเกี่ยวกับ 'อาจจะ' ในคำตอบ:

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

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

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


3

ในซีพียูรุ่นเก่าบางรุ่นจะมีคำสั่งเช่นDJNZ== "ลดลงและกระโดดหากไม่ใช่ศูนย์" สิ่งนี้อนุญาตสำหรับลูปที่มีประสิทธิภาพซึ่งคุณโหลดค่าการนับเริ่มต้นลงในรีจิสเตอร์จากนั้นคุณสามารถจัดการลูปที่ลดลงได้อย่างมีประสิทธิภาพด้วยคำสั่งเดียว เรากำลังพูดถึง ISA ในยุค 80 ที่นี่ครูของคุณจะขาดการติดต่ออย่างจริงจังหากเขาคิดว่า "กฎง่ายๆ" นี้ยังใช้กับซีพียูยุคใหม่ได้


3

บ๊อบ

จนกว่าคุณจะทำ microoptimizations ณ จุดนั้นคุณจะมีคู่มือสำหรับ CPU ของคุณให้พร้อม นอกจากนี้หากคุณกำลังทำสิ่งนั้นคุณอาจไม่จำเป็นต้องถามคำถามนี้อีกต่อไป :-) แต่เห็นได้ชัดว่าครูของคุณไม่ได้สมัครรับแนวคิดนั้น ....

มี 4 สิ่งที่ควรพิจารณาในตัวอย่างลูปของคุณ:

for (i=N; 
 i>=0;             //thing 1
 i--)             //thing 2
{
  putchar('*');   //thing 3
}
  • การเปรียบเทียบ

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

  • การปรับ

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

  • ห่วงร่างกาย

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

  • แคช

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


3

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

กังขา? เพียงแค่เขียนโปรแกรมเพื่อเวลาลูปขึ้น / ลงหน่วยความจำ นี่คือผลลัพธ์ที่ฉันได้รับ:

Average Up Memory   = 4839 mus
Average Down Memory = 5552 mus

Average Up Memory   = 18638 mus
Average Down Memory = 19053 mus

(โดยที่ "mus" ย่อมาจาก microseconds) จากการรันโปรแกรมนี้:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

//Sum all numbers going up memory.
template<class Iterator, class T>
inline void sum_abs_up(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

//Sum all numbers going down memory.
template<class Iterator, class T>
inline void sum_abs_down(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = one_past_last;
  do {
    it--;
    sum += *it;
  } while (it != first);
  total += sum;
}

//Time how long it takes to make num_repititions identical calls to sum_abs_down().
//We will divide this time by num_repitions to get the average time.
template<class T>
std::chrono::nanoseconds TimeDown(std::vector<T> &vec, const std::vector<T> &vec_original,
                                  std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_down(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class T>
std::chrono::nanoseconds TimeUp(std::vector<T> &vec, const std::vector<T> &vec_original,
                                std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_up(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class Iterator, typename T>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) {
  std::random_device rnd_device;
  std::mt19937 generator(rnd_device());
  std::uniform_int_distribution<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) {
  std::random_device rnd_device;
  std::mt19937_64 generator(rnd_device());
  std::uniform_real_distribution<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class ValueType>
void TimeFunctions(std::size_t num_repititions, std::size_t vec_size = (1u << 24)) {
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(vec_size);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up   = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "Average Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Average Down Memory = " << time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  return ;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  TimeFunctions<int>(num_repititions);
  std::cout << '\n';
  TimeFunctions<double>(num_repititions);
  return 0;
}

ทั้งสองอย่างsum_abs_upและsum_abs_downทำสิ่งเดียวกัน (รวมเวกเตอร์ของตัวเลข) และกำหนดเวลาในลักษณะเดียวกันโดยมีข้อแตกต่างเพียงอย่างเดียวคือsum_abs_upเพิ่มหน่วยความจำในขณะที่sum_abs_downหน่วยความจำลดลง ฉันยังผ่านvecการอ้างอิงเพื่อให้ทั้งสองฟังก์ชั่นเข้าถึงตำแหน่งหน่วยความจำเดียวกัน แต่เป็นอย่างต่อเนื่องได้เร็วกว่าsum_abs_up sum_abs_downให้มันวิ่งเอง (ฉันรวบรวมด้วย g ++ -O3)

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

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

FYI vec_originalอยู่ที่นั่นสำหรับการทดลองเพื่อให้ง่ายต่อการเปลี่ยนแปลงsum_abs_upและsum_abs_downในลักษณะที่ทำให้การเปลี่ยนแปลงvecในขณะที่ไม่อนุญาตให้การเปลี่ยนแปลงเหล่านี้มีผลต่อการกำหนดเวลาในอนาคต ผมขอแนะนำให้เล่นรอบกับsum_abs_upและsum_abs_downและระยะเวลาผล


2

โดยไม่คำนึงถึงทิศทางใช้แบบฟอร์มคำนำหน้าเสมอ(++ i แทน i ++)!

for (i=N; i>=0; --i)  

หรือ

for (i=0; i<N; ++i) 

คำอธิบาย: http://www.eskimo.com/~scs/cclass/notes/sx7b.html

นอกจากนี้คุณสามารถเขียน

for (i=N; i; --i)  

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


ไม่เคยเห็นคนบ่นเรื่องนั้นมาก่อน แต่หลังจากอ่านลิงค์มันก็สมเหตุสมผล :) ขอบคุณ
Tommy Jakobsen

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

3
เหตุใดจึงควรใช้แบบฟอร์มคำนำหน้าเสมอ ในกรณีนี้มันมีความหมายเหมือนกัน
Ben Zotto

2
แบบฟอร์ม postfix สามารถสร้างสำเนาที่ไม่จำเป็นของออบเจ็กต์ได้แม้ว่าจะไม่เคยใช้ค่านี้คอมไพลเลอร์อาจปรับให้เหมาะสมกับฟอร์มคำนำหน้าอยู่ดี
Nick Lewis

ฉันมักจะทำด้วยความเคยชิน - i และ i ++ เพราะเมื่อฉันเรียนรู้คอมพิวเตอร์ C มักจะมีการลงทะเบียนล่วงหน้าและภายหลังการเพิ่มจำนวน แต่ไม่ใช่ในทางกลับกัน ดังนั้น * p ++ และ * - p เร็วกว่า * ++ p และ * p-- เนื่องจากทั้งสองแบบเดิมสามารถทำได้ในคำสั่งรหัสเครื่อง 68000 คำสั่ง
JeremyP

2

เป็นคำถามที่น่าสนใจ แต่ในทางปฏิบัติฉันไม่คิดว่ามันสำคัญและไม่ทำให้ลูปหนึ่งดีไปกว่าอีกอัน

อ้างอิงจากหน้าวิกิพีเดียนี้: Leap second "... วันสุริยคติยาวขึ้น 1.7 มิลลิวินาทีทุก ๆ ศตวรรษเนื่องจากแรงเสียดทานของกระแสน้ำเป็นหลัก" แต่ถ้าคุณกำลังนับวันจนถึงวันเกิดคุณสนใจเรื่องเวลาที่แตกต่างกันเล็กน้อยหรือไม่?

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

ฉันจะพนันได้เลยว่าโปรแกรมเมอร์ส่วนใหญ่อ่าน (i = 0; i <N; i ++) และเข้าใจทันทีว่านี่วนซ้ำ N ครั้ง วงของ (i = 1; i <= N; i ++) สำหรับฉันแล้วมันค่อนข้างชัดเจนน้อยกว่าเล็กน้อยและด้วย (i = N; i> 0; i--) ฉันต้องคิดถึงมันสักครู่ . จะเป็นการดีที่สุดหากเจตนาของรหัสเข้าไปในสมองโดยตรงโดยไม่ต้องใช้ความคิดใด ๆ


โครงสร้างทั้งสองเข้าใจง่าย มีบางคนที่อ้างว่าหากคุณมีการทำซ้ำ 3 หรือ 4 ครั้งการคัดลอกคำสั่งนั้นจะดีกว่าการทำวนซ้ำเพราะเข้าใจง่ายกว่า
Danubian Sailor

2

น่าแปลกที่ดูเหมือนว่ามีความแตกต่าง อย่างน้อยใน PHP พิจารณาเกณฑ์มาตรฐานดังต่อไปนี้:

<?php

print "<br>".PHP_VERSION;
$iter = 100000000;
$i=$t1=$t2=0;

$t1 = microtime(true);
for($i=0;$i<$iter;$i++){}
$t2 = microtime(true);
print '<br>$i++ : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;$i--){}
$t2 = microtime(true);
print '<br>$i-- : '.($t2-$t1);

$t1 = microtime(true);
for($i=0;$i<$iter;++$i){}
$t2 = microtime(true);
print '<br>++$i : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;--$i){}
$t2 = microtime(true);
print '<br>--$i : '.($t2-$t1);

ผลลัพธ์น่าสนใจ:

PHP 5.2.13
$i++ : 8.8842368125916
$i-- : 8.1797409057617
++$i : 8.0271911621094
--$i : 7.1027431488037


PHP 5.3.1
$i++ : 8.9625310897827
$i-- : 8.5790238380432
++$i : 5.9647901058197
--$i : 5.4021768569946

ถ้ามีใครรู้สาเหตุก็คงจะดีไม่น้อย :)

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


สาเหตุที่ช้ากว่าคือตัวดำเนินการคำนำหน้าไม่จำเป็นต้องจัดเก็บชั่วคราว พิจารณา $ foo = $ i ++; มีสามสิ่งเกิดขึ้น: $ i ถูกเก็บไว้ที่ชั่วคราว $ i จะเพิ่มขึ้นจากนั้น $ foo จะถูกกำหนดค่าชั่วคราวนั้น ในกรณีของ $ i ++; คอมไพเลอร์อัจฉริยะสามารถตระหนักได้ว่าการชั่วคราวนั้นไม่จำเป็น PHP ไม่เพียง คอมไพเลอร์ C ++ และ Java ฉลาดพอที่จะทำการเพิ่มประสิทธิภาพอย่างง่ายนี้
Conspicuous Compiler

แล้วทำไม $ i - เร็วกว่า $ i ++?
.

คุณใช้เกณฑ์มาตรฐานซ้ำไปกี่ครั้ง คุณตัดคลิป outriders และหาค่าเฉลี่ยสำหรับแต่ละผลลัพธ์หรือไม่? คอมพิวเตอร์ของคุณกำลังทำอย่างอื่นในระหว่างการวัดประสิทธิภาพหรือไม่ ความแตกต่าง ~ 0.5 นั้นอาจเป็นผลมาจากกิจกรรม CPU อื่น ๆ หรือการใช้งานไปป์ไลน์หรือ ... หรือ ... ดีคุณได้รับความคิด
Eight-Bit Guru

ใช่ฉันกำลังให้ค่าเฉลี่ยอยู่ที่นี่ เกณฑ์มาตรฐานถูกเรียกใช้บนเครื่องต่าง ๆ และความแตกต่างเกิดขึ้นโดยบังเอิญ
.

@Conspicuous Compiler => คุณรู้หรือคิด?
.

2

มันสามารถจะเร็วขึ้น

ในโปรเซสเซอร์ NIOS II ที่ฉันกำลังใช้งานอยู่ซึ่งเป็นแบบดั้งเดิมสำหรับลูป

for(i=0;i<100;i++)

ผลิตชุดประกอบ:

ldw r2,-3340(fp) %load i to r2
addi r2,r2,1     %increase i by 1
stw r2,-3340(fp) %save value of i
ldw r2,-3340(fp) %load value again (???)
cmplti r2,r2,100 %compare if less than equal 100
bne r2,zero,0xa018 %jump

ถ้าเรานับถอยหลัง

for(i=100;i--;)

เราได้ชุดประกอบที่ต้องการ 2 คำสั่งน้อยกว่า

ldw r2,-3340(fp)
addi r3,r2,-1
stw r3,-3340(fp)
bne r2,zero,0xa01c

หากเรามีลูปซ้อนกันโดยที่วงในถูกเรียกใช้มากเราจะมีความแตกต่างที่วัดได้:

int i,j,a=0;
for(i=100;i--;){
    for(j=10000;j--;){
        a = j+1;
    }
}

ถ้าวงในเขียนเหมือนข้างบนเวลาดำเนินการคือ 0.12199999999999999734 วินาที ถ้าวงในเขียนแบบเดิมเวลาดำเนินการคือ 0.17199999999999998623 วินาที ดังนั้นการนับถอยหลังของลูปจึงเร็วขึ้นประมาณ30%

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

addi r2,r2,-1
bne r2,zero,0xa01c

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

อย่างไรก็ตามฉันพบว่าบางครั้งถ้า loop body นั้นซับซ้อนเพียงพอคอมไพเลอร์ไม่สามารถทำการเพิ่มประสิทธิภาพนี้ได้ดังนั้นวิธีที่ปลอดภัยที่สุดในการดำเนินการลูปอย่างรวดเร็วคือการเขียน:

register int i;
for(i=10000;i--;)
{ ... }

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


2

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

โดยไม่ต้องใช้ความยาวโดยไม่จำเป็นต้องใช้ตัวนับลูป ฯลฯ สิ่งที่สำคัญด้านล่างนี้เป็นเพียงความเร็วและการนับลูป (ไม่ใช่ศูนย์)

นี่คือวิธีที่คนส่วนใหญ่ใช้การวนซ้ำ 10 ครั้ง:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

สำหรับ 99% ของกรณีทั้งหมดอาจจำเป็นต้องใช้ แต่เมื่อรวมกับ PHP, PYTHON, JavaScript แล้วยังมีซอฟต์แวร์ที่สำคัญทั้งโลก (โดยปกติจะฝังตัว, OS, เกม ฯลฯ ) ที่ CPU เห็บมีความสำคัญมากดังนั้นให้ดูรหัสประกอบของ:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

หลังจากคอมไพล์ (ไม่มีการปรับให้เหมาะสม) เวอร์ชันที่คอมไพล์อาจมีลักษณะเช่นนี้ (VS2015):

-------- C7 45 B0 00 00 00 00  mov         dword ptr [i],0  
-------- EB 09                 jmp         labelB 
labelA   8B 45 B0              mov         eax,dword ptr [i]  
-------- 83 C0 01              add         eax,1  
-------- 89 45 B0              mov         dword ptr [i],eax  
labelB   83 7D B0 0A           cmp         dword ptr [i],0Ah  
-------- 7D 02                 jge         out1 
-------- EB EF                 jmp         labelA  
out1:

ลูปทั้งหมดคือ 8 คำสั่ง (26 ไบต์) ในนั้น - มี 6 คำสั่ง (17 ไบต์) พร้อม 2 สาขา ใช่ใช่ฉันรู้ว่าทำได้ดีกว่านี้ (เป็นเพียงตัวอย่าง)

ลองพิจารณาโครงสร้างที่ใช้บ่อยซึ่งคุณมักจะพบว่าเขียนโดยนักพัฒนาที่ฝังตัว

i = 10;
do
{
    //something here
} while (--i);

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

00074EBC C7 45 B0 01 00 00 00 mov         dword ptr [i],1  
00074EC3 8B 45 B0             mov         eax,dword ptr [i]  
00074EC6 83 E8 01             sub         eax,1  
00074EC9 89 45 B0             mov         dword ptr [i],eax  
00074ECC 75 F5                jne         main+0C3h (074EC3h)  

5 คำสั่ง (18 ไบต์) และเพียงสาขาเดียว จริงๆแล้วมี 4 คำสั่งในลูป (11 ไบต์)

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

00144ECE B9 0A 00 00 00       mov         ecx,0Ah  
label:
                          // something here
00144ED3 E2 FE                loop        label (0144ED3h)  // decrement ecx and jump to label if not zero

ฉันต้องอธิบายว่าอันไหนเร็วกว่า?

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

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

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


1

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

for(int i=myCollection.size(); i >= 0; i--)
{
   ...
}

แต่ถ้ามีความชัดเจนน้อยกว่าที่จะทำแบบนั้นก็ไม่คุ้มค่า ในภาษาสมัยใหม่คุณควรใช้ foreach loop เมื่อเป็นไปได้ คุณพูดถึงกรณีที่คุณควรใช้ foreach loop - เมื่อคุณไม่ต้องการดัชนี


1
ต้องมีความชัดเจนและfor(int i=0, siz=myCollection.size(); i<siz; i++)มีประสิทธิภาพที่คุณควรจะอยู่ในนิสัยของอย่างน้อย
Lawrence Dol

1

ประเด็นก็คือเมื่อนับถอยหลังคุณไม่จำเป็นต้องตรวจสอบi >= 0แยกกันเพื่อลดiจำนวน สังเกต:

for (i = 5; i--;) {
  alert(i);  // alert boxes showing 4, 3, 2, 1, 0
}

ทั้งการเปรียบเทียบและการลดiสามารถทำได้ในนิพจน์เดียว

ดูคำตอบอื่น ๆ สำหรับสาเหตุที่ทำให้คำแนะนำ x86 น้อยลง

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


ฉันคิดว่านี่เป็นรูปแบบที่ไม่ดีเพราะขึ้นอยู่กับผู้อ่านที่รู้ว่าค่าส่งคืนของ i - คือค่าเก่าของ i สำหรับค่าที่เป็นไปได้ของการบันทึกวัฏจักร นั่นจะมีความสำคัญก็ต่อเมื่อมีการวนซ้ำแบบวนซ้ำจำนวนมากและวัฏจักรเป็นส่วนสำคัญของความยาวของการวนซ้ำและปรากฏขึ้นในขณะรันไทม์ ต่อไปจะมีคนพยายามหา (i = 5; --i;) เพราะพวกเขาเคยได้ยินมาว่าใน C ++ คุณอาจต้องการหลีกเลี่ยงการสร้างชั่วคราวเมื่อฉันเป็นประเภทที่ไม่สำคัญและตอนนี้คุณอยู่ในดินแดนที่มีปัญหา โยนความผิดให้โอกาสของคุณที่จะทำให้รหัสผิดดูผิด
mabraham

0

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

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

ดูโค้ด Java ส่วนเล็ก ๆ นี้ (ภาษาไม่สำคัญว่าฉันเดาด้วยเหตุผลนี้):

    System.out.println("top->down");
    int n = 999;
    for (int i = n; i >= 0; i--) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }
    System.out.println("bottom->up");
    n = 1;
    for (int i = 0; i < n; i++) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }

ประเด็นของฉันคือคุณควรพิจารณาเลือกจากบนลงล่างหรือมีค่าคงที่เป็นขอบเขต


ฮะ?!! ตัวอย่างที่คุณล้มเหลวเป็นเรื่องที่ตอบโต้ได้ง่ายจริง ๆ กล่าวคือการโต้เถียงแบบฟาง - ไม่มีใครเคยเขียนสิ่งนี้ for (int i=0; i < 999; i++) {หนึ่งจะเขียน
Lawrence Dol

@Software Monkey จินตนาการว่า n เป็นผลมาจากการคำนวณบางอย่าง ... เช่นคุณอาจต้องการวนซ้ำในคอลเลกชันบางส่วนและขนาดของมันคือขอบเขต แต่เนื่องจากผลข้างเคียงบางอย่างคุณจะเพิ่มองค์ประกอบใหม่ให้กับคอลเลกชันในเนื้อวน
Gabriel Ščerbák

หากนั่นคือสิ่งที่คุณตั้งใจจะสื่อสารนั่นคือสิ่งที่ตัวอย่างของคุณควรแสดงให้เห็น:for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
Lawrence Dol

@Software Monkey ฉันอยากจะเป็นคนทั่วไปมากกว่าแค่พูดถึงคอลเลกชันโดยเฉพาะเพราะสิ่งที่ฉันให้เหตุผลไม่มีส่วนเกี่ยวข้องกับคอลเลกชัน
Gabriel Ščerbák

2
ใช่ แต่ถ้าคุณจะยกตัวอย่างด้วยเหตุผลตัวอย่างของคุณจะต้องมีความน่าเชื่อถือและเป็นตัวอย่างของประเด็น
Lawrence Dol

-1

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

สิ่งนี้จะเป็นจริงมากขึ้นเมื่อจำนวนการวนซ้ำไม่ใช่ค่าคงที่ แต่เป็นตัวแปร

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

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