เหตุใด JVM จึงยังไม่รองรับการเพิ่มประสิทธิภาพการโทรหาง?


95

สองปีหลังจากการเพิ่มประสิทธิภาพdo -the-jvm-Prevent-tail-call-optimizationsดูเหมือนว่าจะมีการนำไปใช้งานต้นแบบ และMLVMได้ระบุคุณลักษณะนี้เป็น "โปรโต 80%" ในบางครั้ง

ไม่มีความสนใจอย่างแข็งขันจากฝั่งของ Sun / Oracle ในการสนับสนุนการโทรหางหรือเป็นเพียงการเรียกหางคือ "[... ] โชคชะตาที่จะมาเป็นอันดับสองในทุกรายการลำดับความสำคัญของคุณลักษณะ [... ]" ตามที่กล่าวไว้ที่JVM การประชุมสุดยอดภาษา ?

ฉันจะสนใจจริงๆถ้ามีคนทดสอบบิลด์ MLVM และสามารถแบ่งปันความประทับใจว่ามันทำงานได้ดีเพียงใด (ถ้าเป็นเช่นนั้น)

อัปเดต: โปรดทราบว่า VM บางตัวเช่นAvianรองรับการโทรหางที่เหมาะสมโดยไม่มีปัญหาใด ๆ


14
ด้วยรายงานการอพยพของชาวซันจาก Oracle ฉันไม่คาดหวังว่าโครงการใด ๆในปัจจุบันจะดำเนินต่อไปเว้นแต่จะกล่าวอย่างชัดเจนจาก Oracle :(
Thorbjørn Ravn Andersen

16
โปรดทราบว่าคำตอบที่คุณยอมรับนั้นผิดทั้งหมด ไม่มีข้อขัดแย้งพื้นฐานระหว่างการเพิ่มประสิทธิภาพการโทรหางและ OOP และแน่นอนหลายภาษาเช่น OCaml และ F # มีทั้ง OOP และ TCO
JD

2
การเรียกภาษา OCaml และ F # OOP เป็นเรื่องตลกร้ายตั้งแต่แรก แต่ใช่ OOP และ TCO ไม่มีอะไรเหมือนกันมากนักยกเว้นความจริงที่ว่ารันไทม์ต้องตรวจสอบว่าเมธอดที่ปรับให้เหมาะสมนั้นไม่ได้ถูกแทนที่ / คลาสย่อยที่อื่น
soc

5
+1 มาจากพื้นหลัง C ฉันคิดเสมอว่า TCO เป็นสิ่งที่กำหนดใน JVM สมัยใหม่ มันไม่เคยเกิดขึ้นกับผมจริงตรวจสอบและเมื่อฉันได้ผลลัพธ์ที่น่าแปลกใจ ...
thkala

2
@soc: "ยกเว้นความจริงที่ว่ารันไทม์ต้องตรวจสอบว่าเมธอดที่กำลังปรับให้เหมาะสมนั้นไม่ถูกแทนที่ / คลาสย่อยที่อื่น" "ข้อเท็จจริง" ของคุณเป็นเรื่องไร้สาระโดยสิ้นเชิง
JD

คำตอบ:


33

การวินิจฉัยรหัส Java: การปรับปรุงประสิทธิภาพของ Java Code ของคุณ ( alt ) อธิบายว่าเหตุใด JVM จึงไม่สนับสนุนการเพิ่มประสิทธิภาพการโทรหาง

แต่ถึงแม้ว่าจะเป็นที่ทราบกันดีว่าจะแปลงฟังก์ชัน tail-recursive โดยอัตโนมัติให้เป็น loop แบบธรรมดาได้อย่างไร แต่ข้อกำหนดของ Java ไม่จำเป็นต้องทำการแปลง สันนิษฐานว่าสาเหตุหนึ่งที่ไม่ใช่ข้อกำหนดก็คือโดยทั่วไปแล้วการเปลี่ยนแปลงไม่สามารถทำแบบคงที่ในภาษาเชิงวัตถุได้ แต่การแปลงจากฟังก์ชัน tail-recursive ไปเป็น simple loop จะต้องทำแบบไดนามิกโดยคอมไพเลอร์ JIT

จากนั้นให้ตัวอย่างโค้ด Java ที่จะไม่แปลง

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

จากนั้นจะให้การทดสอบที่คุณสามารถใช้เพื่อดูว่า JIT ของคุณทำสิ่งนี้หรือไม่

โดยปกติแล้วเนื่องจากเป็นกระดาษ IBM จึงมีปลั๊ก:

ฉันรันโปรแกรมนี้ด้วย Java SDK สองสามตัวและผลลัพธ์ก็น่าประหลาดใจ การทำงานบน Sun's Hotspot JVM สำหรับเวอร์ชัน 1.3 เผยให้เห็นว่า Hotspot ไม่ได้ทำการเปลี่ยนแปลง ที่การตั้งค่าเริ่มต้นพื้นที่สแต็กจะหมดภายในเวลาไม่ถึงหนึ่งวินาทีบนเครื่องของฉัน ในทางกลับกัน JVM ของไอบีเอ็มสำหรับเวอร์ชัน 1.3 ส่งเสียงฟี้อย่างไม่มีปัญหาแสดงว่ามันแปลงรหัสด้วยวิธีนี้


63
FWIW การโทรหางไม่ใช่แค่เกี่ยวกับฟังก์ชันเรียกซ้ำในตัวเองอย่างที่เขาบอก การเรียกหางคือการเรียกฟังก์ชันใด ๆ ที่ปรากฏในตำแหน่งหาง พวกเขาไม่จำเป็นต้องเรียกตัวเองและไม่จำเป็นต้องถูกเรียกไปยังตำแหน่งที่รู้จักแบบคงที่ (เช่นสามารถเรียกใช้วิธีเสมือนได้) ปัญหาที่เขาอธิบายนั้นไม่ใช่ปัญหาหากการเพิ่มประสิทธิภาพการโทรหางทำได้อย่างถูกต้องในกรณีทั่วไปและด้วยเหตุนี้ตัวอย่างของเขาจึงทำงานได้อย่างสมบูรณ์ในภาษาเชิงวัตถุที่รองรับการเรียกหาง (เช่น OCaml และ F #)
JD

3
"ต้องทำแบบไดนามิกโดยคอมไพเลอร์ JIT" ซึ่งหมายความว่าต้องทำโดย JVM เองแทนที่จะเป็นคอมไพเลอร์ Java แต่ OP กำลังถามเกี่ยวกับ JVM
Raedwald

11
"โดยทั่วไปแล้วการแปลงจะไม่สามารถสร้างแบบคงที่ในภาษาเชิงวัตถุได้" แน่นอนว่านี่เป็นคำพูด แต่ทุกครั้งที่ฉันเห็นข้ออ้างเช่นนี้ฉันอยากจะถามเกี่ยวกับตัวเลข - เพราะฉันจะไม่แปลกใจถ้าในทางปฏิบัติในกรณีส่วนใหญ่สามารถกำหนดได้ในเวลารวบรวม
greenoldman

5
ตอนนี้ลิงก์ไปยังบทความที่อ้างถึงไม่สามารถใช้งานได้แม้ว่า Google จะแคชไว้ก็ตาม ที่สำคัญกว่านั้นการให้เหตุผลของผู้เขียนผิดพลาด ตัวอย่างที่ให้ไว้อาจเป็น tail-call ที่ปรับให้เหมาะสมโดยใช้การคอมไพล์แบบคงที่และไม่ใช่แค่การคอมไพล์แบบไดนามิกหากคอมไพลเลอร์แทรกการinstanceofตรวจสอบเพื่อดูว่าthisเป็นExampleอ็อบเจ็กต์หรือExampleไม่
Alex D

5
เพียงลิงค์ไปที่ webarchive web.archive.org/web/20120506085636/http://www.ibm.com/…
Suvitruf - Andrei Apanasik

30

เหตุผลหนึ่งที่ฉันเคยเห็นมาก่อนในการไม่ใช้ TCO (และถูกมองว่าเป็นเรื่องยาก) ใน Java คือรูปแบบการอนุญาตใน JVM นั้นมีความไวต่อสแต็กดังนั้นการโทรหางจึงต้องจัดการด้านความปลอดภัย

ฉันเชื่อว่าสิ่งนี้แสดงให้เห็นแล้วว่า Clements และ Felleisen ไม่เป็นอุปสรรค [1] [2] และฉันค่อนข้างมั่นใจว่าแพตช์ MLVM ที่กล่าวถึงในคำถามนั้นเกี่ยวข้องด้วยเช่นกัน

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

  1. http://www.ccs.neu.edu/scheme/pubs/esop2003-cf.pdf
  2. http://www.ccs.neu.edu/scheme/pubs/cf-toplas04.pdf

2
+1. ฟังคำถาม / คำตอบในตอนท้ายของการนำเสนอของ Brian Goetz youtube.com/watch?v=2y5Pv4yN0b0&t=3739
mcoolive

15

บางทีคุณอาจจะรู้เรื่องนี้อยู่แล้ว แต่คุณลักษณะนี้ไม่สำคัญเท่าที่ควรเนื่องจากภาษา Java เปิดเผยการติดตามสแต็กกับโปรแกรมเมอร์

พิจารณาโปรแกรมต่อไปนี้:

public class Test {

    public static String f() {
        String s = Math.random() > .5 ? f() : g();
        return s;
    }

    public static String g() {
        if (Math.random() > .9) {
            StackTraceElement[] ste = new Throwable().getStackTrace();
            return ste[ste.length / 2].getMethodName();
        }
        return f();
    }

    public static void main(String[] args) {
        System.out.println(f());
    }
}

แม้ว่าจะมี "tail-call" แต่ก็อาจไม่ได้รับการปรับให้เหมาะสม (หากมีการปรับให้เหมาะสมที่สุดก็ยังคงต้องมีการจัดเก็บหนังสือของ call-stack ทั้งหมดเนื่องจากความหมายของโปรแกรมขึ้นอยู่กับมัน)

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


17
พบข้อผิดพลาดในความคิดของคุณ: "ต้องมีการเก็บหนังสือของ call-stack ทั้งหมดเนื่องจากความหมายของโปรแกรมขึ้นอยู่กับมัน" :-) เหมือนกับ "ข้อยกเว้นที่ถูกระงับ" ใหม่ โปรแกรมที่อาศัยสิ่งเหล่านี้จะถูกทำลาย ในความคิดของฉันพฤติกรรมของโปรแกรมนั้นถูกต้องอย่างยิ่ง: การทิ้งสแต็กเฟรมเป็นสิ่งที่เรียกหางทั้งหมดเกี่ยวกับ
soc

4
@Marco แต่วิธีการใด ๆ ที่สามารถทำให้เกิดข้อยกเว้นซึ่ง call-stack ทั้งหมดถูกผูกไว้ให้พร้อมใช้งานใช่ไหม? นอกจากนี้คุณไม่สามารถตัดสินใจได้ล่วงหน้าว่าวิธีใดจะเรียกโดยอ้อมgในกรณีนี้ ... ลองนึกถึงความหลากหลายและการสะท้อน
aioobe

2
เป็นผลข้างเคียงที่เกิดจากการเพิ่ม ARM ใน Java 7 ซึ่งเป็นตัวอย่างที่คุณไม่สามารถพึ่งพาสิ่งต่างๆที่คุณได้แสดงไว้ข้างต้นได้
soc

6
"ความจริงที่ว่าภาษาเปิดเผย call-stack ทำให้ยากต่อการนำไปใช้": ภาษาต้องการให้ stack-trace ที่ส่งคืนgetStackTrace()จากวิธีการx()ที่ซอร์สโค้ดแสดงเรียกจากวิธีการที่y()แสดงว่าx()ถูกเรียกด้วยy()หรือไม่? ถ้ามีเสรีภาพก็ไม่มีปัญหาจริงๆ
Raedwald

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

12

Java เป็นภาษาที่ใช้งานได้น้อยที่สุดที่คุณอาจจินตนาการได้ (โอเคอาจจะไม่ใช่ !) แต่นี่จะเป็นข้อได้เปรียบที่ดีสำหรับภาษา JVM เช่นScalaซึ่งก็คือ

ข้อสังเกตของฉันคือการทำให้ JVM เป็นแพลตฟอร์มสำหรับภาษาอื่นดูเหมือนจะไม่เคยอยู่ในอันดับต้น ๆ ของรายการลำดับความสำคัญของ Sun และฉันเดาว่าตอนนี้สำหรับ Oracle


16
@ Thorbjørn - ฉันเขียนโปรแกรมเพื่อทำนายว่าโปรแกรมใด ๆ จะหยุดลงในระยะเวลาที่ จำกัด ฉันใช้เวลานาน !
oxbow_lakes

3
พื้นฐานแรกที่ฉันใช้ไม่มีฟังก์ชัน แต่เป็น GOSUB และ RETURN ฉันไม่คิดว่า LOLCODE จะใช้งานได้ดีเช่นกัน (และคุณสามารถรับมันได้ด้วยสองประสาทสัมผัส)
David Thornley

1
@David, functional! = มีฟังก์ชัน
Thorbjørn Ravn Andersen

2
@ Thorbjørn Ravn Andersen: ไม่ แต่มันเป็นข้อกำหนดเบื้องต้นคุณจะไม่พูดเหรอ?
David Thornley

3
"การทำให้ JVM เป็นแพลตฟอร์มสำหรับภาษาอื่น ๆ ดูเหมือนจะไม่เคยอยู่ในอันดับต้น ๆ ของรายการลำดับความสำคัญของ Sun" พวกเขาใช้ความพยายามอย่างมากในการทำให้ JVM เป็นแพลตฟอร์มสำหรับภาษาแบบไดนามิกมากกว่าภาษาที่ใช้งานได้
JD
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.