การเพิ่มประสิทธิภาพการโทรหางมีอยู่ในหลายภาษาและคอมไพเลอร์ ในสถานการณ์นี้คอมไพเลอร์รับรู้ถึงฟังก์ชั่นของแบบฟอร์ม:
int foo(n) {
...
return bar(n);
}
ที่นี่ภาษาสามารถรับรู้ว่าผลลัพธ์ที่ส่งคืนมาเป็นผลลัพธ์จากฟังก์ชั่นอื่นและเปลี่ยนการเรียกใช้ฟังก์ชันด้วยสแต็กเฟรมใหม่เป็นการกระโดด
ตระหนักดีว่าวิธีการแบบแฟคทอเรียลแบบคลาสสิก:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
คือไม่ optimizatable โทรหางเพราะของการตรวจสอบที่จำเป็นในการกลับมา ( ตัวอย่างซอร์สโค้ดและเอาต์พุตที่คอมไพล์แล้ว )
ในการทำให้การเรียก tail นี้สามารถปรับได้
int _fact(int n, int acc) {
if(n == 1) return acc;
return _fact(n - 1, acc * n);
}
int factorial(int n) {
if(n == 0) return 1;
return _fact(n, 1);
}
การคอมไพล์โค้ดนี้ด้วยgcc -O2 -S fact.c
(-O2 จำเป็นต้องเปิดใช้งานการปรับให้เหมาะสมในคอมไพเลอร์ แต่ด้วยการเพิ่มประสิทธิภาพที่ -O3 มันจะยากสำหรับมนุษย์ที่จะอ่าน ... )
_fact(int, int):
cmpl $1, %edi
movl %esi, %eax
je .L2
.L3:
imull %edi, %eax
subl $1, %edi
cmpl $1, %edi
jne .L3
.L2:
rep ret
( ตัวอย่างซอร์สโค้ดและเอาต์พุตที่คอมไพล์แล้ว )
หนึ่งสามารถดูในส่วน.L3
ที่jne
มากกว่าcall
(ซึ่งไม่ได้โทร subroutine กับกองกรอบใหม่)
โปรดทราบว่าสิ่งนี้ทำกับ C. Tail call optimization ใน Java นั้นยากและขึ้นอยู่กับการใช้งาน JVM (ที่กล่าวว่าฉันไม่เคยเห็นมันทำเพราะมันยากและความหมายของโมเดลการรักษาความปลอดภัย Java ที่ต้องการเฟรมสแต็ก - ซึ่งเป็นสิ่งที่หลีกเลี่ยง TCO) - tail-recursion + javaและtail-recursion + optimizationเป็นชุดแท็กที่ดีในการเรียกดู คุณอาจพบว่าภาษา JVM อื่น ๆ สามารถปรับการเรียกซ้ำแบบหางให้ดีขึ้นได้ (ลอง clojure (ซึ่งต้องใช้การเพิ่มประสิทธิภาพการเรียกกลับหาง) หรือสกาล่า)
ที่กล่าวว่า
มีความสุขบางอย่างในการรู้ว่าคุณเขียนบางสิ่งถูกต้อง - ในวิธีอุดมคติที่สามารถทำได้
และตอนนี้ฉันจะได้รับสก็อตและวางอิเล็คทรอนิกส์เยอรมันบางส่วน ...
สำหรับคำถามทั่วไปของ "วิธีการเพื่อหลีกเลี่ยงการโอเวอร์โฟลว์สแต็กในอัลกอริทึมแบบเรียกซ้ำ" ...
อีกวิธีคือการรวมตัวนับการเรียกซ้ำ นี่คือการตรวจจับลูปไม่สิ้นสุดที่เกิดจากสถานการณ์ที่อยู่นอกเหนือการควบคุมของคน (และการเข้ารหัสที่ไม่ดี)
ตัวนับการเรียกซ้ำใช้รูปแบบของ
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
ทุกครั้งที่คุณโทรคุณจะเพิ่มเคาน์เตอร์ หากตัวนับใหญ่เกินไปคุณผิดพลาด (ที่นี่เพียงคืน -1 แต่ในภาษาอื่นที่คุณอาจต้องการยกเว้น) ความคิดคือการป้องกันไม่ให้สิ่งเลวร้ายเกิดขึ้น (จากข้อผิดพลาดของหน่วยความจำ) เมื่อทำการเรียกซ้ำที่ลึกกว่าที่คาดไว้มาก
ในทางทฤษฎีคุณไม่ควรต้องการสิ่งนี้ ในทางปฏิบัติฉันได้เห็นโค้ดที่เขียนไม่ดีซึ่งมีผลกระทบนี้เนื่องจากมีข้อผิดพลาดเล็ก ๆ น้อย ๆ และการเขียนโค้ดที่ไม่ดี (ปัญหาการเกิดพร้อมกันหลายเธรดแบบหลายเธรดที่มีบางสิ่งเปลี่ยนแปลงบางอย่าง
ใช้อัลกอริทึมที่เหมาะสมและแก้ปัญหาที่ถูกต้อง โดยเฉพาะสำหรับ Collatz Conjecture ดูเหมือนว่าคุณกำลังพยายามแก้ไขมันด้วยวิธีxkcd :
คุณกำลังเริ่มต้นที่ตัวเลขและทำการสำรวจเส้นทางต้นไม้ สิ่งนี้นำไปสู่พื้นที่การค้นหาที่มีขนาดใหญ่มากอย่างรวดเร็ว การวิ่งอย่างรวดเร็วเพื่อคำนวณจำนวนการวนซ้ำสำหรับผลลัพธ์คำตอบที่ถูกต้องในขั้นตอนประมาณ 500 ขั้น นี่ไม่ควรเป็นปัญหาสำหรับการเรียกซ้ำด้วยกรอบสแต็กขนาดเล็ก
ในขณะที่รู้วิธีการแก้ปัญหา recursive ไม่ได้เป็นสิ่งที่ไม่ดีหนึ่งยังควรตระหนักว่าหลายครั้งการแก้ปัญหาซ้ำแล้วซ้ำอีกจะดีกว่า จำนวนของวิธีการของการแสวงหาแปลงขั้นตอนวิธีการเรียกซ้ำไปซ้ำหนึ่งสามารถมองเห็นในกองมากเกินที่ทางที่จะไปจากการเรียกซ้ำไปซ้ำ