JVM มีข้อ จำกัด อะไรในการเพิ่มประสิทธิภาพการโทรแบบหาง


36

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

คอมไพเลอร์ Scala สามารถดำเนินการ TCO สำหรับฟังก์ชันแบบเรียกซ้ำ แต่ไม่ใช่สำหรับสองฟังก์ชันแบบเรียกซ้ำ

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

def approximate(guess: Double): Double =
  if (isGoodEnough(guess)) guess
  else approximate(improve(guess))

ถูกแปลเป็น

0: aload_0
1: astore_3
2: aload_0
3: dload_1
4: invokevirtual #24; //Method isGoodEnough:(D)Z
7: ifeq
10: dload_1
11: dreturn
12: aload_0
13: dload_1
14: invokevirtual #27; //Method improve:(D)D
17: dstore_1
18: goto 2

ดังนั้นในระดับ bytecode gotoหนึ่งความต้องการเพียง ในกรณีนี้ในความเป็นจริงการทำงานอย่างหนักจะทำโดยคอมไพเลอร์

สิ่งอำนวยความสะดวกใดของเครื่องเสมือนพื้นฐานที่ยอมให้คอมไพเลอร์จัดการกับ TCO ได้ง่ายขึ้น

ในฐานะที่เป็นบันทึกย่อฉันไม่คาดหวังว่าเครื่องจักรจริงจะฉลาดกว่า JVM มากนัก ถึงกระนั้นหลายภาษาที่คอมไพล์รหัสเนทีฟเช่น Haskell ดูเหมือนจะไม่มีปัญหากับการปรับ tail tail (เช่นกัน Haskell อาจมีบางครั้งเนื่องจากความเกียจคร้าน แต่นั่นเป็นอีกปัญหาหนึ่ง)

คำตอบ:


25

ตอนนี้ฉันไม่รู้อะไรเกี่ยวกับ Clojure และ Scala น้อย แต่ฉันจะให้มันยิง

ก่อนอื่นเราต้องแยกความแตกต่างระหว่าง tail-call และ tail-recursion การเรียกซ้ำแบบหางค่อนข้างง่ายที่จะเปลี่ยนเป็นลูป ด้วยการโทรหางมันเป็นเรื่องยากมากที่จะเป็นไปไม่ได้ในกรณีทั่วไป คุณจำเป็นต้องรู้สิ่งที่ถูกเรียก แต่ด้วยความหลากหลายและ / หรือฟังก์ชั่นชั้นหนึ่งคุณไม่ค่อยรู้ว่าดังนั้นคอมไพเลอร์ไม่สามารถรู้วิธีแทนที่การโทร เฉพาะตอนรันไทม์ที่คุณรู้รหัสเป้าหมายและสามารถข้ามไปได้โดยไม่ต้องจัดสรรเฟรมสแต็กอื่น ตัวอย่างเช่นแฟรกเมนต์ต่อไปนี้มี tail call และไม่ต้องการพื้นที่สแต็กเมื่อปรับให้เหมาะสม (รวมถึง TCO) อย่างเหมาะสม แต่ก็ไม่สามารถกำจัดได้เมื่อรวบรวม JVM:

function forward(obj: Callable<int, int>, arg: int) =
    let arg1 <- arg + 1 in obj.call(arg1)

ในขณะที่มันไม่มีประสิทธิภาพที่นี่มีรูปแบบการเขียนโปรแกรมทั้งหมด (เช่น Continuation Passing Style หรือ CPS) ที่มีการเรียกหางจำนวนมากและไม่ค่อยกลับมา การทำเช่นนั้นโดยไม่มี TCO เต็มหมายความว่าคุณสามารถเรียกใช้โค้ดขนาดจิ๋วเพียงเล็กน้อยก่อนที่จะหมดพื้นที่สแต็ก

สิ่งอำนวยความสะดวกใดของเครื่องเสมือนพื้นฐานที่ยอมให้คอมไพเลอร์จัดการกับ TCO ได้ง่ายขึ้น

คำสั่ง tail tail เช่นใน Lua 5.1 VM ตัวอย่างของคุณไม่ได้ง่ายกว่านี้มากนัก ฉันกลายเป็นอย่างนี้:

push arg
push 1
add
load obj
tailcall Callable.call
// implicit return; stack frame was recycled

ในฐานะ sidenote ฉันไม่คิดว่าเครื่องจักรจริงจะฉลาดกว่า JVM มากนัก

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


ฉันเห็น. ฉันไม่ได้ตระหนักว่าการมีความฉลาดน้อยกว่าอาจทำให้เกิดการเพิ่มประสิทธิภาพซึ่งเป็นสิ่งต้องห้าม
Andrea

7
+1 tailcallเรียนการสอนสำหรับ JVM ได้รับการเสนออยู่แล้วเป็นช่วงต้น 2007: บล็อกบน sun.com ผ่านเครื่อง หลังจากการครอบครองของออราเคิลลิงค์นี้ 404 ฉันเดาว่ามันไม่ได้ทำให้เป็นรายการลำดับความสำคัญของ JVM 7
K.Steff

1
การtailcallเรียนการสอนจะทำเครื่องหมายเพียงโทรหางเป็นโทรหาง ไม่ว่าจะเป็น JVM จริงแล้วปรับให้เหมาะสมพูดว่าโทรหางเป็นคำถามที่แตกต่างอย่างสิ้นเชิง CLI CIL มี.tailคำนำหน้าคำแนะนำ แต่ Microsoft 64- บิต CLR เป็นเวลานานไม่ได้ปรับให้เหมาะสม OTOH ไอบีเอ็ม J9 JVM ไม่ตรวจสอบสายหางและเพิ่มประสิทธิภาพของพวกเขาโดยไม่จำเป็นต้องเป็นคำสั่งพิเศษที่จะบอกว่าที่โทรสายหาง คำอธิบายประกอบการโทรหางและการเพิ่มประสิทธิภาพการโทรหางเป็นมุมฉากจริงๆ (นอกเหนือจากข้อเท็จจริงที่ว่าการอนุมานแบบคงที่ซึ่งการโทรแบบหางอาจจะเป็นแบบนั้นหรืออาจจะไม่สามารถบอกได้ว่า Dunno)
Jörg W Mittag

@ JörgWMittagคุณทำให้จุดที่ดีเป็น JVM call something; oreturnสามารถตรวจสอบรูปแบบได้อย่างง่ายดาย งานหลักของการอัพเดตข้อมูลจำเพาะ JVM จะไม่แนะนำคำสั่ง tail-call อย่างชัดเจน แต่เพื่อยืนยันว่าคำสั่งนั้นได้รับการปรับให้เหมาะสม คำสั่งดังกล่าวช่วยให้งานของนักเขียนคอมไพเลอร์ง่ายขึ้นเท่านั้น: ผู้เขียน JVM ไม่จำเป็นต้องจำลำดับการสอนนั้นก่อนที่จะได้รับการรวบรวมและการคอมไพเลอร์ X-> bytecode สามารถมั่นใจได้ว่า bytecode นั้นไม่ถูกต้องหรือ เพิ่มประสิทธิภาพจริงไม่เคยแก้ไข แต่สแต็ค - มากเกินไป

@delnan: ลำดับcall something; return;จะเทียบเท่ากับการเรียกหางหากสิ่งที่เรียกว่าไม่เคยถามสำหรับกองติดตาม; หากวิธีการดังกล่าวเป็นเสมือนหรือเรียกใช้วิธีเสมือน JVM จะไม่มีทางรู้ว่าจะสอบถามเกี่ยวกับสแต็กได้หรือไม่
supercat

12

Clojure สามารถทำการปรับหางแบบวนซ้ำอัตโนมัติให้เป็นลูปได้อย่างแน่นอนมันเป็นไปได้ที่จะทำเช่นนี้กับ JVM ตามที่ Scala พิสูจน์

จริงๆแล้วมันเป็นการตัดสินใจออกแบบที่จะไม่ทำสิ่งนี้ - คุณต้องใช้recurแบบฟอร์มพิเศษถ้าคุณต้องการคุณสมบัตินี้ ดูเธรดอีเมลRe: ทำไมไม่มีการเพิ่มประสิทธิภาพการโทรหางในกลุ่ม Clojure google

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

การทำซ้ำ JVM ในอนาคตมีแนวโน้มที่จะได้รับความสามารถนี้แม้ว่าอาจเป็นตัวเลือกเพื่อให้สามารถใช้งานได้กับโค้ดเก่าได้ กล่าวว่าคุณลักษณะดูตัวอย่างที่ Geeknizer แสดงรายการนี้สำหรับ Java 9:

การเพิ่มการโทรหางและการต่อเนื่อง ...

แน่นอนแผนการในอนาคตอาจเปลี่ยนแปลงได้เสมอ

มันกลับกลายเป็นว่าไม่ใช่เรื่องใหญ่อะไรอยู่ดี ในช่วง 2 ปีของการเข้ารหัส Clojure ฉันไม่เคยพบเจอสถานการณ์ที่การขาด TCO เป็นปัญหา สาเหตุหลักคือ:

  • คุณสามารถเรียกซ้ำหางอย่างรวดเร็วได้แล้วถึง 99% ของกรณีทั่วไปที่มีrecurหรือวนซ้ำ กรณีการเรียกซ้ำหางซึ่งกันและกันนั้นค่อนข้างหายากในรหัสปกติ
  • แม้ว่าคุณต้องการการเรียกซ้ำแบบร่วมกันบ่อยครั้งความลึกของการเรียกซ้ำนั้นค่อนข้างตื้นเขินที่คุณสามารถทำได้บนสแต็กโดยที่ไม่มี TCO TCO เป็นเพียง "การเพิ่มประสิทธิภาพ" หลังจากทั้งหมด ....
  • ในกรณีที่หายากมากที่คุณต้องการรูปแบบการเรียกร้องร่วมกันที่ไม่ใช้เวลานานมีทางเลือกอื่น ๆ มากมายที่สามารถบรรลุเป้าหมายเดียวกัน: ลำดับที่ขี้เกียจ, แทมโพลีน ฯลฯ

"การทำซ้ำในอนาคต" - ดูตัวอย่างคุณสมบัติที่ Geeknizer พูดว่าสำหรับ Java 9: ​​การเพิ่มการโทรหางและการดำเนินการต่อ - มันคืออะไร?
ริ้น

1
ใช่ - แค่นั้นแหละ แน่นอนการจดแต้มในอนาคตอาจมีการเปลี่ยนแปลงได้เสมอ ...
mikera

5

ในฐานะ sidenote ฉันไม่คิดว่าเครื่องจักรจริงจะฉลาดกว่า JVM มากนัก

มันไม่ได้เกี่ยวกับความฉลาด แต่เกี่ยวกับความแตกต่าง จนกระทั่งเมื่อไม่นานมานี้ JVM ได้รับการออกแบบและปรับให้เหมาะสมสำหรับภาษาเดียว (Java, ชัด ๆ ) ซึ่งมีหน่วยความจำและรูปแบบการโทรที่เข้มงวดมาก

ไม่เพียง แต่ไม่มีgotoตัวชี้หรือยังไม่มีวิธีเรียกฟังก์ชัน 'bare' (วิธีที่ไม่ได้กำหนดไว้ในคลาส)

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

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

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


3

ดังนั้นในระดับ bytecode หนึ่งต้องการเพียงแค่ goto ในกรณีนี้ในความเป็นจริงการทำงานหนักจะทำโดยคอมไพเลอร์

คุณสังเกตเห็นว่าที่อยู่ของวิธีการเริ่มต้นด้วย 0 หรือไม่ ที่ทุกวิธี ofsets เริ่มต้นด้วย 0? JVM ไม่อนุญาตให้กระโดดข้ามวิธี

ฉันไม่รู้ว่าจะเกิดอะไรขึ้นกับสาขาที่มีออฟเซ็ตนอกวิธีที่โหลดโดย java - บางทีมันอาจจะถูก byifier verifier ตรวจจับบางทีมันอาจจะสร้างข้อยกเว้นและบางทีมันอาจจะกระโดดนอกวิธี

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


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