"JVM ไม่รองรับการปรับให้เหมาะสมแบบหางเรียกดังนั้นฉันทำนายสแต็กการระเบิดจำนวนมาก"
ใครก็ตามที่บอกว่าสิ่งนี้ (1) ไม่เข้าใจการเพิ่มประสิทธิภาพการโทรแบบหางหรือ (2) ไม่เข้าใจ JVM หรือ (3) ทั้งคู่
ฉันจะเริ่มด้วยคำนิยามของ tail-call จากWikipedia (ถ้าคุณไม่ชอบ Wikipedia นี่เป็นทางเลือก ):
ในวิทยาการคอมพิวเตอร์หางเรียกเป็นการเรียกรูทีนย่อยที่เกิดขึ้นภายในกระบวนการอื่นเป็นการกระทำขั้นสุดท้าย มันอาจสร้างค่าตอบแทนซึ่งจะส่งกลับทันทีโดยขั้นตอนการโทร
ในรหัสด้านล่างการเรียกเพื่อbar()
เป็นการเรียกหางของfoo()
:
private void foo() {
// do something
bar()
}
การเพิ่มประสิทธิภาพการโทรท้ายเกิดขึ้นเมื่อการใช้ภาษาเห็นการโทรหางไม่ได้ใช้วิธีการเรียกปกติ (ซึ่งสร้างกรอบสแต็ก) แต่สร้างสาขาแทน นี่คือการปรับให้เหมาะสมเนื่องจากเฟรมสแต็กต้องการหน่วยความจำและต้องใช้รอบ CPU เพื่อส่งข้อมูล (เช่นที่อยู่ที่ส่งคืน) ไปยังเฟรมและเนื่องจากคู่การโทร / ส่งคืนจะต้องใช้วงจร CPU มากกว่าการกระโดดแบบไม่มีเงื่อนไข
TCO มักใช้กับการเรียกซ้ำ แต่ไม่ได้ใช้เพียงอย่างเดียว ไม่สามารถใช้ได้กับการเรียกซ้ำทั้งหมด ตัวอย่างของการคำนวณแบบแฟกทอเรียลแบบง่าย ๆ ไม่สามารถทำการโทรแบบหางได้เพราะสิ่งสุดท้ายที่เกิดขึ้นในฟังก์ชั่นคือการดำเนินการคูณ
public static int fact(int n) {
if (n <= 1) return 1;
else return n * fact(n - 1);
}
ในการใช้การเพิ่มประสิทธิภาพการโทรหางคุณต้องมีสองสิ่ง:
- แพลตฟอร์มที่รองรับการแยกย่อยนอกเหนือจากการเรียกรูทีนย่อย
- ตัววิเคราะห์แบบสแตติกที่สามารถพิจารณาได้ว่าการเพิ่มประสิทธิภาพการโทรหางเป็นไปได้หรือไม่
แค่นั้นแหละ. ดังที่ฉันได้บันทึกไว้ที่อื่น JVM (เช่นสถาปัตยกรรมทัวริงที่สมบูรณ์อื่น ๆ ) มีการข้ามไป มันเกิดขึ้นที่จะมีการข้ามไปแบบไม่มีเงื่อนไขแต่การใช้งานสามารถทำได้ง่าย ๆ โดยใช้สาขาที่มีเงื่อนไข
ชิ้นส่วนการวิเคราะห์แบบคงที่คือสิ่งที่ยุ่งยาก ภายในฟังก์ชั่นเดียวก็ไม่มีปัญหา ตัวอย่างเช่นต่อไปนี้เป็นฟังก์ชัน Scala แบบเรียกซ้ำเพื่อรวมค่าในList
:
def sum(acc:Int, list:List[Int]) : Int = {
if (list.isEmpty) acc
else sum(acc + list.head, list.tail)
}
ฟังก์ชั่นนี้จะกลายเป็น bytecode ต่อไปนี้:
public int sum(int, scala.collection.immutable.List);
Code:
0: aload_2
1: invokevirtual #63; //Method scala/collection/immutable/List.isEmpty:()Z
4: ifeq 9
7: iload_1
8: ireturn
9: iload_1
10: aload_2
11: invokevirtual #67; //Method scala/collection/immutable/List.head:()Ljava/lang/Object;
14: invokestatic #73; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
17: iadd
18: aload_2
19: invokevirtual #76; //Method scala/collection/immutable/List.tail:()Ljava/lang/Object;
22: checkcast #59; //class scala/collection/immutable/List
25: astore_2
26: istore_1
27: goto 0
สังเกตgoto 0
ที่ท้าย โดยการเปรียบเทียบฟังก์ชั่น Java เทียบเท่า (ซึ่งจะต้องใช้Iterator
เพื่อเลียนแบบพฤติกรรมของการแบ่งรายการ Scala เป็นหัวและหาง) กลายเป็น bytecode ต่อไปนี้ หมายเหตุที่สองการดำเนินงานที่ผ่านมาในขณะนี้เป็นวิงวอนขอตามมาด้วยผลตอบแทนที่ชัดเจนของค่าที่ผลิตโดยที่การภาวนา recursive
public static int sum(int, java.util.Iterator);
Code:
0: aload_1
1: invokeinterface #64, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z
6: ifne 11
9: iload_0
10: ireturn
11: iload_0
12: aload_1
13: invokeinterface #70, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
18: checkcast #25; //class java/lang/Integer
21: invokevirtual #74; //Method java/lang/Integer.intValue:()I
24: iadd
25: aload_1
26: invokestatic #43; //Method sum:(ILjava/util/Iterator;)I
29: ireturn
หางเพิ่มประสิทธิภาพการเรียกร้องของฟังก์ชั่นเดียวจิ๊บจ๊อย: คอมไพเลอร์จะเห็นว่ามีเป็นรหัสที่ใช้ผลของการโทรไม่ดังนั้นมันสามารถแทนที่วิงวอนgoto
ด้วย
ชีวิตจะยุ่งยากหากคุณมีหลายวิธี คำแนะนำการแยกสาขาของ JVM ซึ่งแตกต่างจากตัวประมวลผลที่ใช้งานทั่วไปเช่น 80x86 นั้น จำกัด อยู่ที่วิธีการเดียว มันยังค่อนข้างตรงไปตรงมาถ้าคุณมีวิธีการส่วนตัว: คอมไพเลอร์มีอิสระในการแทรกวิธีการเหล่านั้นตามความเหมาะสมดังนั้นสามารถเพิ่มประสิทธิภาพการโทรหาง (หากคุณสงสัยว่ามันทำงานอย่างไรให้พิจารณาวิธีการทั่วไปที่ใช้การswitch
ควบคุมพฤติกรรม) คุณสามารถขยายเทคนิคนี้ไปยังวิธีสาธารณะหลายวิธีในคลาสเดียวกัน: คอมไพเลอร์อินไลน์เนื้อความวิธีการให้วิธีการสะพานสาธารณะและการโทรภายในกลายเป็นกระโดด
แต่รุ่นนี้พังลงเมื่อคุณพิจารณาวิธีการสาธารณะในชั้นเรียนที่แตกต่างกันโดยเฉพาะในแง่ของอินเทอร์เฟซและตัวโหลดคลาส คอมไพเลอร์ระดับซอร์สนั้นไม่มีความรู้เพียงพอที่จะใช้การปรับให้เหมาะสมแบบหางเรียก อย่างไรก็ตามแตกต่างจากการใช้งาน "โลหะเปลือย", * JVM (มีข้อมูลที่จะทำเช่นนี้ในรูปแบบของ Hotspot คอมไพเลอร์ (อย่างน้อย, คอมไพเลอร์อดีตดวงอาทิตย์) ฉันไม่รู้ว่ามันจริงหรือไม่ การเพิ่มประสิทธิภาพหางโทรและผู้ต้องสงสัยไม่ได้ แต่ก็สามารถทำได้
ข้อใดนำฉันมาที่ส่วนที่สองของคำถามของคุณซึ่งฉันจะใช้ถ้อยคำใหม่ว่า
เห็นได้ชัดว่าหากภาษาของคุณใช้การเรียกซ้ำเป็นภาษาดั้งเดิม แต่เพียงผู้เดียวสำหรับการทำซ้ำคุณก็สนใจ แต่ภาษาที่ต้องการคุณสมบัตินี้สามารถใช้งานได้ ปัญหาเดียวคือว่าคอมไพเลอร์สำหรับภาษาดังกล่าวสามารถผลิตคลาสที่สามารถโทรและถูกเรียกโดยคลาส Java โดยพลการ
นอกเหนือจากกรณีนี้ฉันจะเชิญผู้ลงคะแนนด้วยการบอกว่าไม่เกี่ยวข้อง ส่วนใหญ่ของรหัส recursive ที่ผมเคยเห็น (และผมเคยทำงานที่มีจำนวนมากของโครงการกราฟ) ไม่ optimizable เช่นแฟคทอเรียลธรรมดามันใช้การเรียกซ้ำเพื่อสร้างสถานะและการดำเนินการหางเป็นการรวมกัน
สำหรับรหัสที่สามารถปรับได้ตามความเหมาะสม tail-มักจะตรงไปตรงมาเพื่อแปลรหัสนั้นในรูปแบบ iterable ตัวอย่างเช่นsum()
ฟังก์ชั่นที่ฉันแสดงให้เห็นก่อนหน้านี้สามารถเป็นแบบทั่วไปfoldLeft()
ได้ หากคุณดูที่แหล่งข้อมูลคุณจะเห็นว่ามีการนำไปใช้จริงเป็นการดำเนินการซ้ำ ๆ Jörg W Mittagมีตัวอย่างของเครื่องสถานะที่ใช้ผ่านการเรียกใช้ฟังก์ชัน มีการใช้งานเครื่องรัฐอย่างมีประสิทธิภาพ (และบำรุงรักษาได้) ที่ไม่พึ่งพาการเรียกใช้ฟังก์ชันที่ถูกแปลเป็นการข้าม
ฉันจะจบสิ่งที่แตกต่างอย่างสิ้นเชิง หากคุณ Google วิธีการของคุณจากเชิงอรรถใน SICP คุณอาจจะจบลงที่นี่ ผมเองพบว่าเป็นสถานที่ที่น่าสนใจมากขึ้นกว่าที่มีคอมไพเลอร์ของฉันแทนที่โดยJSR
JUMP