เหตุใดลูปจึงเร็วกว่าการเรียกซ้ำ


18

ในทางปฏิบัติฉันเข้าใจว่าการเรียกซ้ำใด ๆ สามารถเขียนเป็นลูป (และในทางกลับกัน (?)) และหากเราวัดด้วยคอมพิวเตอร์จริงเราพบว่าลูปนั้นเร็วกว่าการเรียกซ้ำสำหรับปัญหาเดียวกัน แต่มีทฤษฎีอะไรที่ทำให้เกิดความแตกต่างนี้


9
รูปลักษณ์นั้นเร็วกว่าการเรียกซ้ำในภาษาที่ใช้งานได้ไม่ดีเท่านั้น ในภาษาที่มี Tail Recursion ที่เหมาะสมโปรแกรม recursive สามารถแปลเป็นลูปหลังฉากซึ่งในกรณีนี้จะไม่มีความแตกต่างเพราะมันเหมือนกัน
jmite

3
ใช่และถ้าคุณใช้ภาษาที่รองรับคุณสามารถใช้การเรียกซ้ำ (หาง) โดยไม่ส่งผลกระทบด้านลบต่อประสิทธิภาพการทำงาน
jmite

1
@jmite, recursion หางที่สามารถเป็นจริงได้รับการปรับเข้าสู่วงเป็นของหายากเหลือเกินที่ทำได้ยากยิ่งกว่าที่คุณคิด โดยเฉพาะอย่างยิ่งในภาษาที่มีประเภทการจัดการเช่นตัวแปรนับอ้างอิง
Johan - คืนสถานะโมนิก้า

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

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

คำตอบ:


17

เหตุผลที่ลูปเร็วกว่าการเรียกซ้ำเป็นเรื่องง่าย
การวนซ้ำมีลักษณะเช่นนี้ในชุดประกอบ

mov loopcounter,i
dowork:/do work
dec loopcounter
jmp_if_not_zero dowork

การกระโดดตามเงื่อนไขเดียวและการบันทึกบัญชีสำหรับตัวนับลูป

การเรียกซ้ำ (เมื่อไม่ได้หรือไม่สามารถปรับให้เหมาะสมโดยคอมไพเลอร์) มีลักษณะดังนี้:

start_subroutine:
pop parameter1
pop parameter2
dowork://dowork
test something
jmp_if_true done
push parameter1
push parameter2
call start_subroutine
done:ret

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

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

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

ตัวอย่างของการจัดการการเรียกซ้ำที่ไม่ดี ตัวอย่างเช่นหากพารามิเตอร์ถูกส่งผ่านที่นับการอ้างอิง (เช่นพารามิเตอร์ประเภทที่ไม่ได้จัดการ const) มันจะเพิ่ม 100 รอบที่ทำการปรับการล็อกการนับการอ้างอิงทั้งหมดฆ่าประสิทธิภาพเทียบกับลูป
ในภาษาที่ได้รับการปรับเพื่อเรียกซ้ำพฤติกรรมที่ไม่ดีนี้จะไม่เกิดขึ้น

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

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

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

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

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

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

ฉันเข้าใจว่าการเรียกซ้ำใด ๆ สามารถเขียนเป็นลูปได้

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


16

คำตอบอื่น ๆ เหล่านี้ค่อนข้างทำให้เข้าใจผิด ฉันยอมรับว่าพวกเขาระบุรายละเอียดการปฏิบัติที่สามารถอธิบายความแตกต่างนี้ แต่พวกเขาคุยโวเรื่องนี้มากเกินไป เป็นข้อเสนอแนะอย่างถูกต้องโดย jmite พวกเขามีการดำเนินงานที่มุ่งเน้นไปยังเสียการใช้งานฟังก์ชั่นของสาย / recursion หลายภาษาใช้ลูปผ่านการเรียกซ้ำดังนั้นลูปจะไม่ชัดเจนในภาษาเหล่านั้น การเรียกซ้ำไม่มีประสิทธิภาพน้อยกว่าการวนซ้ำ (เมื่อใช้ทั้งคู่) ในทางทฤษฎี ให้ฉันพูดบทคัดย่อเพื่อกระดาษ 1977 Guy Steele Debunking "การเรียกขั้นตอนแพง" ตำนานหรือการดำเนินการตามขั้นตอนถือว่าเป็นอันตรายหรือแลมบ์ดา: GOTO ที่สุด

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

"ความขัดแย้งระหว่างแนวคิดการเขียนโปรแกรมนามธรรมและภาษาโครงสร้างคอนกรีต" จะเห็นได้จากความจริงที่ว่าแบบจำลองทางทฤษฎีส่วนใหญ่เช่นที่ untyped แคลคูลัสแลมบ์ดา , ไม่ได้มีสแต็ค แน่นอนความขัดแย้งนี้ไม่จำเป็นตามที่อธิบายไว้ข้างต้นและยังแสดงให้เห็นโดยภาษาที่ไม่มีกลไกการทำซ้ำนอกเหนือจากการเรียกซ้ำเช่น Haskell

fixfix f x = f (fix f) x(λx.M)ยังไม่มีข้อความM[ยังไม่มีข้อความ/x][ยังไม่มีข้อความ/x]xMยังไม่มีข้อความ

ตอนนี้เป็นตัวอย่าง กำหนดfactเป็น

fact = fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1

นี่คือการประเมินผลของfact 3ที่ไหนแน่นฉันจะใช้gเป็นคำพ้องสำหรับเช่นfix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) fact = g 1สิ่งนี้ไม่มีผลต่อการโต้แย้งของฉัน

fact 3 
~> g 1 3
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1 3 
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 1 3
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 1 3
~> (λn.if n == 0 then 1 else g (1*n) (n-1)) 3
~> if 3 == 0 then 1 else g (1*3) (3-1)
~> g (1*3) (3-1)
~> g 3 2
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 3 2
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 3 2
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 3 2
~> (λn.if n == 0 then 3 else g (3*n) (n-1)) 2
~> if 2 == 0 then 3 else g (3*2) (2-1)
~> g (3*2) (2-1)
~> g 6 1
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 1
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 1
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 1
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 1
~> if 1 == 0 then 6 else g (6*1) (1-1)
~> g (6*1) (1-1)
~> g 6 0
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 0
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 0
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 0
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 0
~> if 0 == 0 then 6 else g (6*0) (0-1)
~> 6

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

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

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

ดังนั้นการตอบสนองต่อคำตอบของ Veedrac สแต็คเป็นไม่ได้ "พื้นฐาน" เพื่อเรียกซ้ำ เท่าที่พฤติกรรม "เหมือนกอง" เกิดขึ้นในระหว่างการประเมินผลสิ่งนี้สามารถเกิดขึ้นได้ในกรณีที่ลูป (ไม่มีโครงสร้างข้อมูลเสริม) จะไม่สามารถใช้ได้ตั้งแต่แรก! เพื่อให้เป็นอีกวิธีหนึ่งฉันสามารถใช้ลูปกับการเรียกซ้ำด้วยลักษณะการทำงานที่เหมือนกันทุกประการ แน่นอนทั้ง Scheme และ SML ประกอบด้วยโครงสร้างวนลูป แต่ทั้งคู่กำหนดนิยามในรูปแบบของการเรียกซ้ำ (และอย่างน้อยใน Scheme doมักจะถูกนำมาใช้เป็นมาโครที่ขยายเป็นการเรียกซ้ำ) ในทำนองเดียวกันสำหรับคำตอบของ Johan คอมไพเลอร์ต้องปล่อยแอสเซมบลีโจฮานที่อธิบายไว้สำหรับการเรียกซ้ำ อันที่จริงชุดประกอบเดียวกันทุกประการไม่ว่าคุณจะใช้ลูปหรือเรียกซ้ำ มีเพียงครั้งเดียวที่คอมไพเลอร์จะ ( ข้อผูกมัดปล่อยออกมา) เพื่อปล่อยแอสเซมบลีอย่างที่ Johan อธิบายคือเมื่อคุณกำลังทำบางสิ่งที่ไม่สามารถแสดงได้โดยลูป ตามที่ระบุไว้ในกระดาษของสตีลและแสดงให้เห็นโดยการปฏิบัติจริงของภาษาเช่น Haskell, โครงการ SML และก็ไม่ได้เป็น "หายาก" ว่าสายหางสามารถ "ที่ดีที่สุด" ที่พวกเขาสามารถเสมอเป็น "ปรับ" การเรียกใช้การเรียกซ้ำโดยเฉพาะจะทำงานในพื้นที่คงที่หรือไม่นั้นขึ้นอยู่กับวิธีการเขียน แต่ข้อ จำกัด ที่คุณต้องใช้เพื่อให้เป็นไปได้นั้นเป็นข้อ จำกัด ที่คุณต้องการเพื่อให้พอดีกับปัญหาของคุณในรูปแบบของลูป (ที่จริงพวกเขามีความเข้มงวดน้อย. มีปัญหาเช่นการเข้ารหัสเครื่องของรัฐที่มีมากขึ้นอย่างเรียบร้อยและมีประสิทธิภาพการจัดการผ่านทางหางสายเมื่อเทียบกับลูปซึ่งจะต้องมีตัวแปรเสริมมี.) อีกครั้งเรียกซ้ำครั้งเดียวที่ต้องทำมากขึ้นการทำงาน เมื่อรหัสของคุณไม่วนซ้ำอยู่ดี

ฉันเดาว่า Johan หมายถึงคอมไพเลอร์ C ที่มีข้อ จำกัด โดยพลการเมื่อมันจะทำการ tail tail "optimization" Johan ยังกล่าวถึงภาษาเช่น C ++ และ Rust เมื่อเขาพูดถึง "ภาษาที่มีประเภทที่มีการจัดการ" RAIIสำนวนจาก C ++ และปัจจุบันใน Rust เช่นกันทำให้สิ่งที่มีลักษณะเช่นเผินโทรหางไม่สายหาง (เพราะ "destructors" ยังคงต้องเรียกว่า) มีข้อเสนอที่จะใช้ไวยากรณ์ที่แตกต่างเพื่อเลือกในความหมายที่แตกต่างกันเล็กน้อยที่จะอนุญาตให้เรียกซ้ำหาง (เช่น destructors การโทรก่อนการเรียกหางสุดท้ายและไม่อนุญาตให้เข้าถึงวัตถุ "ถูกทำลาย" อย่างชัดเจน (การรวบรวมขยะไม่มีปัญหาดังกล่าวและ Haskell, SML และ Scheme ทั้งหมดเป็นภาษาที่รวบรวมขยะ) ในหลอดเลือดดำที่แตกต่างกันมากบางภาษาเช่น Smalltalk เปิดเผย "สแต็ก" เป็นวัตถุชั้นหนึ่งในสิ่งเหล่านี้ ในกรณีที่ "สแต็ค" นั้นไม่ได้มีรายละเอียดการใช้งานอีกต่อไปแม้ว่าจะไม่ได้แยกการเรียกสายที่มีความหมายต่างกัน (Java กล่าวว่าไม่สามารถทำได้เนื่องจากวิธีการจัดการด้านความปลอดภัย แต่นี่เป็นเรื่องจริงเท็จ )

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


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

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

O(1)

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

คำตอบที่น่าสนใจมาก แม้ว่ามันฟังดูจะรุนแรงไปหน่อย :-) โหวตขึ้นเพราะฉันเรียนรู้สิ่งใหม่
Johan - คืนสถานะโมนิก้า

2

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

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

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