ทำไม Java จึงไม่มีการปรับให้เหมาะสำหรับการเรียกซ้ำทั้งหมด


92

จากสิ่งที่ฉันได้อ่าน: เหตุผลก็เพราะมันไม่ง่ายที่จะตัดสินว่าวิธีการใดที่จะถูกเรียกตามที่เราได้รับมรดก

อย่างไรก็ตามทำไม Java อย่างน้อยก็ไม่มีการปรับให้เหมาะสมแบบเรียกซ้ำสำหรับเมธอดแบบสแตติกและบังคับใช้วิธีที่เหมาะสมในการเรียกเมธอดแบบสแตติกกับคอมไพเลอร์?

ทำไม Java ไม่ได้รับการสนับสนุนใด ๆ เลยสำหรับการเรียกซ้ำ?

ฉันไม่แน่ใจว่ามีปัญหาใด ๆ หรือไม่


เกี่ยวกับคำแนะนำซ้ำตามที่อธิบายโดยJörg W Mittag 1 :

  • อีกคำถามหนึ่งถามเกี่ยวกับ TCO คำถามนี้เกี่ยวกับ TRE TRE นั้นง่ายกว่า TCO มาก
  • นอกจากนี้คำถามอื่นถามเกี่ยวกับข้อ จำกัด ที่ JVM กำหนดในการปรับใช้ภาษาที่ต้องการรวบรวม JVM คำถามนี้ถามเกี่ยวกับ Java ซึ่งเป็นภาษาหนึ่งที่ไม่ได้ถูก จำกัด โดย JVM เนื่องจาก JVM spec สามารถเปลี่ยนแปลงได้โดย คนเดียวกันที่ออกแบบ Java
  • และสุดท้ายไม่มีข้อ จำกัด ใน JVM เกี่ยวกับ TRE เพราะ JVM มีวิธีการภายใน GOTO ซึ่งเป็นสิ่งที่จำเป็นสำหรับ TRE

1 เพิ่มการจัดรูปแบบเพื่อเรียกจุดที่ทำ


26
ในการอ้างถึงEric Lippert : "ฟีเจอร์ไม่ถูกพวกมันมีราคาแพงมากและไม่เพียง แต่ต้องแสดงให้เห็นถึงต้นทุนของตัวเองเท่านั้นพวกเขายังต้องแสดงให้เห็นถึงต้นทุนค่าเสียโอกาสในการไม่ทำคุณสมบัติอื่น ๆ อีกร้อยอย่างที่เราทำได้ Java ได้รับการออกแบบมาเพื่อดึงดูดนักพัฒนา C / C ++ และการปรับการเรียกซ้ำแบบหางกลับไม่รับประกันในภาษาเหล่านั้น
Doval

5
แก้ไขให้ถูกต้องหากฉันผิด แต่ Eric Lippert เป็นนักออกแบบสำหรับ C # ซึ่งมีการปรับการเรียกซ้ำหางให้เหมาะสมหรือไม่
InformedA

1
เขาอยู่ในทีมคอมไพเลอร์ C # ใช่
Doval

10
จากสิ่งที่ฉันเข้าใจJIT อาจทำมัน , ถ้าเงื่อนไขตรงอาจจะ ดังนั้นในทางปฏิบัติคุณไม่สามารถวางใจได้ แต่ไม่ว่า C # จะทำหรือไม่มีจุดสำคัญของ Eric
Doval

4
@InstructedA หากคุณขุดลึกลงไปอีกนิดคุณจะเห็นว่ามันไม่เคยทำในคอมไพเลอร์ JIT แบบ 32 บิต JIT 64 บิตนั้นใหม่กว่าและชาญฉลาดกว่าในหลาย ๆ ด้าน คอมไพเลอร์แบบทดลองรุ่นใหม่ (ทั้งสำหรับ 32 และ 64 บิต) นั้นชาญฉลาดกว่าและจะสนับสนุนการปรับให้เหมาะสมแบบเรียกซ้ำใน IL ซึ่งไม่ได้ขออย่างชัดเจน มีอีกประเด็นที่คุณต้องคำนึงถึง - ผู้รวบรวม JIT ไม่มีเวลามาก พวกเขาได้รับการปรับให้เหมาะสมอย่างมากสำหรับความเร็ว - แอปพลิเคชันที่อาจใช้เวลาหลายชั่วโมงในการรวบรวมใน C ++ จะยังคงต้องเปลี่ยนจาก IL เป็นภาษาท้องถิ่นในสองร้อยมิลลิวินาที (อย่างน้อยที่สุด)
Luaan

คำตอบ:


132

ตามที่อธิบายโดย Brian Goetz (สถาปนิกภาษา Java ที่ Oracle) ในวิดีโอนี้:

ในคลาส jdk [... ] มีวิธีการรักษาความปลอดภัยจำนวนหนึ่งที่พึ่งพาการนับสแต็กเฟรมระหว่างรหัสไลบรารี jdk และรหัสการโทรเพื่อหาว่าใครกำลังโทรเข้ามา

อะไรก็ตามที่เปลี่ยนจำนวนเฟรมในสแต็กจะทำให้เกิดปัญหานี้และจะทำให้เกิดข้อผิดพลาด เขายอมรับว่านี่เป็นเหตุผลที่โง่เง่าและนักพัฒนาซอฟต์แวร์ JDK ก็เข้ามาแทนที่กลไกนี้

จากนั้นเขาก็กล่าวว่ามันไม่ใช่ลำดับความสำคัญ แต่หางที่เรียกซ้ำ

ในที่สุดก็จะเสร็จ

NB สิ่งนี้ใช้กับ HotSpot และ OpenJDK, VM อื่น ๆ อาจแตกต่างกันไป


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

1
ทำไมไม่ใช้วิธีแก้ปัญหา? เหมือน label-goto ภายใต้ปกที่เพิ่งข้ามไปด้านบนของการเรียกเมธอดด้วยค่าใหม่สำหรับอาร์กิวเมนต์? นี่เป็นการรวบรวมเวลาที่เหมาะสมและไม่จำเป็นต้องเปลี่ยนเฟรมสแต็กหรือก่อให้เกิดการละเมิดความปลอดภัย
James Watkins

2
มันจะดีกว่ามากสำหรับเราถ้าคุณหรือคนอื่น ๆ ที่นี่สามารถเจาะลึกลงไปและให้เฉพาะสิ่งที่ "วิธีการรักษาความปลอดภัยที่มีความสำคัญ" เหล่านั้น ขอขอบคุณ!
InformedA

3
@InstructedA - ดูsecuringjava.com/chapter-three/chapter-three-6.htmlซึ่งมีรายละเอียดของวิธีการที่ระบบจัดการการรักษาความปลอดภัย Java ทำงานประมาณปล่อยของ Java 2
จูลส์

3
วิธีแก้ปัญหาหนึ่งคือใช้ภาษา JVM อื่น ตัวอย่างเช่น Scala ทำสิ่งนี้ในคอมไพเลอร์ไม่ใช่ตอนรันไทม์
James Moore

24

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

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


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

4
@amon เจมส์กอสลิงเคยบอกว่าเขาจะไม่เพิ่มคุณสมบัติให้ Java เว้นแต่คนหลายคนร้องขอและเพียงแล้วเขาจะพิจารณามัน ดังนั้นผมจะไม่แปลกใจถ้าเป็นส่วนหนึ่งของคำตอบจริงๆก็คือ "คุณก็สามารถใช้forห่วง" (และเช่นเดียวกันสำหรับฟังก์ชั่นชั้นแรกเทียบกับวัตถุ) ฉันจะไม่ไปไกลถึงเรียกมันว่า "ความดื้อรั้นอย่างรุนแรง" แต่ฉันไม่คิดว่ามันจะเป็นที่ต้องการสูงมากในปี 1995 เมื่อความกังวลสูงสุดอาจเป็นความเร็วของ Java และการขาดยาชื่อสามัญ
Doval

7
@ มอนมงต์โมเดลความปลอดภัยทำให้ฉันเป็นเหตุผลที่ถูกต้องที่จะไม่เพิ่ม TCO ลงในภาษาที่มีอยู่ก่อน แต่มีเหตุผลที่ไม่ดีที่จะไม่ออกแบบมันเป็นภาษาแรก คุณไม่ได้โยนคุณสมบัติที่สำคัญของโปรแกรมเมอร์เพื่อที่จะรวมคุณสมบัติย่อย ๆ ไว้เบื้องหลัง "ทำไม Java 8 ถึงไม่มี TCO" เป็นคำถามที่แตกต่างจาก "ทำไม Java 1.0 ถึงไม่มี TCO" ฉันกำลังตอบหลัง
Karl Bielefeldt

3
@rwong คุณสามารถเปลี่ยนใด ๆฟังก์ชันเวียนไปซ้ำหนึ่ง (ถ้าฉันอาจฉันได้เขียนตัวอย่างที่นี่ )
Alain

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

5

Java ไม่มีการเพิ่มประสิทธิภาพการโทรสูงเนื่องจาก JVM ไม่มี bytecode สำหรับการโทรแบบหาง (ไปยังตัวชี้ฟังก์ชันที่ไม่รู้จักแบบคงที่บางตัวเช่นวิธีการใน vtable บางตัว)

ดูเหมือนว่าด้วยเหตุผลทางสังคม (และบางทีอาจเป็นทางเทคนิค) การเพิ่มการดำเนินการ bytecode ใหม่ใน JVM (ซึ่งจะทำให้ขัดกับ JVM รุ่นก่อนหน้านี้) เป็นเรื่องยากมากสำหรับเจ้าของ JVM spec

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

การเรียกท้ายไปยังฟังก์ชั่นที่ไม่รู้จักต้องเปลี่ยนเฟรมสแต็กปัจจุบันด้วยอันใหม่และการดำเนินการนั้นควรอยู่ใน JVM (ไม่ใช่เพียงเรื่องของการเปลี่ยนคอมไพเลอร์สร้าง bytecode)


17
คำถามไม่ได้เกี่ยวกับการโทรหาง แต่เป็นการเรียกซ้ำหาง และคำถามไม่ได้เกี่ยวกับภาษา JVM bytecode แต่เป็นเรื่องเกี่ยวกับภาษาการเขียนโปรแกรม Java ซึ่งเป็นภาษาที่แตกต่างอย่างสิ้นเชิง สกาล่าคอมไพล์ให้กับ JVM bytecode (ในหมู่อื่น ๆ ) และมีการกำจัดการเรียกซ้ำ การใช้งานโครงการบน JVM มีการโทรแบบเต็มรูปแบบที่เหมาะสม Java 7 (JVM Spec, 3rd Ed.) เพิ่ม bytecode ใหม่ IBM J9 JVM ดำเนินการกับ TCO ได้โดยไม่ต้องใช้โค้ดพิเศษ
Jörg W Mittag

1
@ JörgWMittag: จริง แต่ฉันสงสัยว่ามันเป็นความจริงที่ว่าภาษาการเขียนโปรแกรม Java ไม่มีการเรียกหางที่เหมาะสม มันอาจจะแม่นยำมากกว่าที่จะระบุว่าภาษาการเขียนโปรแกรมจาวาไม่จำเป็นต้องมีการเรียกแบบหางที่เหมาะสม (นั่นคือผมไม่แน่ใจว่าสิ่งที่อยู่ในสเปคจริงห้ามการดำเนินงานจากการขจัดโทรหางมันก็ไม่ได้กล่าวถึง..)
ruakh

4

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

คาดว่าเครื่องอาจมีขนาดสแต็กแตกต่างกันและดังนั้นจึงเป็นไปได้สำหรับรหัสที่จะทำงานกับเครื่องหนึ่ง แต่ระเบิดกองอื่น โดยทั่วไปแล้วโค้ดที่จะใช้กับเครื่องหนึ่งแม้ว่าสแต็คจะมีการบีบอัดแบบเทียมจะทำงานกับเครื่องทั้งหมดที่มีขนาดสแต็ค "ปกติ" อย่างไรก็ตามหากวิธีการที่เรียกซ้ำลึก 1,000,000 นั้นเป็นแบบ tail-call ที่ได้รับการปรับให้เหมาะสมกับเครื่องหนึ่ง แต่ไม่ใช่วิธีอื่นการประมวลผลบนเครื่องเดิมจะทำงานได้แม้ว่ามันจะมีขนาดเล็กผิดปกติและล้มเหลวหลังแม้ว่าจะมีขนาดใหญ่ผิดปกติก็ตาม .


2

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

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