Tại sao Java không tối ưu hóa cho đệ quy đuôi?


92

Từ những gì tôi đã đọc: Lý do là vì không dễ để xác định phương thức nào sẽ thực sự được gọi là chúng ta có sự kế thừa.

Tuy nhiên, tại sao Java ít nhất không có tối ưu hóa đệ quy đuôi cho các phương thức tĩnh và thực thi cách thức phù hợp để gọi các phương thức tĩnh với trình biên dịch?

Tại sao Java không có bất kỳ sự hỗ trợ nào cho đệ quy đuôi?

Tôi không chắc chắn nếu có bất kỳ khó khăn ở đây.


Về bản sao được đề xuất , như được giải thích bởi Jörg W Mittag 1 :

  • Câu hỏi khác hỏi về TCO, câu hỏi này về TRE. TRE đơn giản hơn nhiều so với TCO.
  • Ngoài ra, câu hỏi khác hỏi về những hạn chế mà JVM áp đặt đối với việc triển khai ngôn ngữ muốn biên dịch sang JVM, câu hỏi này hỏi về Java, đây là ngôn ngữ không bị hạn chế bởi JVM, vì thông số JVM có thể được thay đổi bởi Những người cùng thiết kế Java.
  • Và cuối cùng, thậm chí không có hạn chế nào trong JVM về TRE, bởi vì JVM không có GOTO nội bộ, đó là tất cả những gì cần thiết cho TRE

1 Định dạng được thêm vào để gọi ra các điểm được thực hiện.


26
Trích lời Eric Lippert : "Các tính năng không rẻ, chúng cực kỳ đắt đỏ và chúng không chỉ phải tự biện minh cho chi phí của mình, mà còn phải chứng minh chi phí cơ hội của việc không thực hiện hàng trăm tính năng khác mà chúng tôi có thể thực hiện với ngân sách đó." Java được thiết kế một phần để thu hút các nhà phát triển C / C ++ và tối ưu hóa đệ quy đuôi không được đảm bảo trong các ngôn ngữ đó.
Doval 4/2/2015

5
Sửa lỗi cho tôi nếu tôi sai, nhưng Eric Lippert có phải là nhà thiết kế cho C # có tối ưu hóa đệ quy đuôi không?
thông báo

1
Anh ấy thuộc nhóm biên dịch C #, vâng.
Doval 4/2/2015

10
Từ những gì tôi hiểu, JIT có thể làm điều đó , nếu điều kiện nhất định được đáp ứng , có thể. Vì vậy, trong thực tế, bạn không thể dựa vào nó. Nhưng cho dù C # có hay không có thì nó cũng không quan trọng đối với quan điểm của Eric.
Doval 4/2/2015

4
@InstructedA Nếu bạn đào sâu hơn một chút, bạn có thể thấy rằng nó chưa bao giờ được thực hiện trong trình biên dịch JIT 32 bit. JIT 64 bit mới hơn và thông minh hơn theo nhiều cách. Trình biên dịch thử nghiệm mới hơn (cả cho 32 và 64 bit) vẫn thông minh hơn và sẽ hỗ trợ tối ưu hóa đệ quy đuôi trong IL mà không yêu cầu rõ ràng về nó. Có một điểm khác bạn cần tính đến - trình biên dịch JIT không có nhiều thời gian. Chúng được tối ưu hóa rất nhiều cho tốc độ - ứng dụng có thể mất hàng giờ để biên dịch trong C ++ vẫn sẽ cần phải chuyển từ IL sang bản địa trong vài trăm ms, ít nhất là (ít nhất là một phần).
Luaan

Câu trả lời:


132

Theo giải thích của Brian Goetz (Kiến trúc sư ngôn ngữ Java tại Oracle) trong video này :

trong các lớp jdk [...] có một số phương thức nhạy cảm bảo mật dựa vào việc đếm các khung ngăn xếp giữa mã thư viện jdk và mã gọi để tìm ra ai gọi chúng.

Bất cứ điều gì thay đổi số lượng khung hình trên ngăn xếp sẽ phá vỡ điều này và sẽ gây ra lỗi. Ông thừa nhận đây là một lý do ngu ngốc, và vì vậy các nhà phát triển JDK đã thay thế cơ chế này.

Sau đó, ông đề cập rằng đó không phải là một ưu tiên, nhưng đệ quy đuôi đó

cuối cùng sẽ hoàn thành

NB Điều này áp dụng cho HotSpot và OpenJDK, các VM khác có thể khác nhau.


7
Tôi ngạc nhiên khi có một câu trả lời khá khô khan như thế này! Nhưng nó thực sự giống như Câu trả lời - hiện tại có thể, vì những lý do kỹ thuật lỗi thời mà nó chưa được thực hiện, và vì vậy bây giờ chúng tôi chỉ chờ ai đó quyết định nó đủ quan trọng để thực hiện.
BrianH

1
Tại sao không thực hiện một cách giải quyết? Giống như một nhãn-goto dưới vỏ bọc chỉ nhảy lên đầu cuộc gọi phương thức với các giá trị mới cho các đối số? Đây sẽ là một tối ưu hóa thời gian biên dịch và sẽ không cần phải thay đổi các khung ngăn xếp hoặc gây ra vi phạm bảo mật.
James Watkins

2
Sẽ tốt hơn nhiều cho chúng tôi nếu bạn hoặc ai đó ở đây có thể đào sâu hơn và cung cấp cụ thể những "phương pháp nhạy cảm bảo mật" đó là gì. Cảm ơn bạn!
Được thông báo vào

3
@InstructedA - xem securingjava.com/chapter-three/chapter-three-6.html chứa một mô tả chi tiết về cách hệ thống Java Security Manager làm việc khoảng sự ra đời của Java 2.
Jules

3
Một cách giải quyết khác là sử dụng ngôn ngữ JVM khác. Scala, ví dụ, thực hiện điều này trong trình biên dịch, không phải trong thời gian chạy.
James Moore

24

Java không có tối ưu hóa cuộc gọi đuôi vì lý do tương tự hầu hết các ngôn ngữ bắt buộc không có nó. Các vòng lặp bắt buộc là phong cách ưa thích của ngôn ngữ và lập trình viên có thể thay thế đệ quy đuôi bằng các vòng lặp bắt buộc. Sự phức tạp không đáng có đối với một tính năng mà việc sử dụng không được khuyến khích là vấn đề về phong cách.

Điều này mà các lập trình viên đôi khi muốn viết theo kiểu FP bằng các ngôn ngữ bắt buộc khác chỉ xuất hiện trong 10 năm gần đây, sau khi máy tính bắt đầu nhân rộng trong lõi thay vì GHz. Ngay cả bây giờ, nó không phải là phổ biến. Nếu tôi đề nghị thay thế một vòng lặp bắt buộc bằng đệ quy đuôi tại nơi làm việc, một nửa những người đánh giá mã sẽ cười và nửa còn lại sẽ đưa ra một cái nhìn bối rối. Ngay cả trong lập trình chức năng, bạn thường tránh đệ quy đuôi trừ khi các cấu trúc khác như các hàm bậc cao hơn không khớp hoàn toàn.


36
Câu trả lời này có vẻ không đúng. Chỉ vì đệ quy đuôi không thực sự cần thiết không có nghĩa là nó sẽ không hữu ích. Các giải pháp đệ quy thường dễ hiểu hơn lặp lại, nhưng việc thiếu các lệnh gọi đuôi có nghĩa là các thuật toán đệ quy trở nên không chính xác đối với các kích thước bài toán lớn sẽ thổi tung ngăn xếp. Đây là về tính chính xác, không phải hiệu suất (tốc độ giao dịch cho đơn giản thường đáng giá). Như câu trả lời chính xác lưu ý, việc thiếu các cuộc gọi đuôi là do một mô hình bảo mật kỳ lạ dựa vào dấu vết ngăn xếp, chứ không phải do sự cố chấp bắt buộc.
amon

4
@amon James Gosling đã từng nói rằng anh ta sẽ không thêm một tính năng vào Java trừ khi nhiều người yêu cầu nó và chỉ sau đó anh ta mới xem xét nó. Vì vậy, tôi sẽ không ngạc nhiên nếu một phần của câu trả lời thực sự là "bạn luôn có thể sử dụng một forvòng lặp" (và tương tự như vậy đối với các hàm hạng nhất so với các đối tượng). Tôi sẽ không đi xa đến mức gọi nó là "sự cố chấp bắt buộc" nhưng tôi không nghĩ rằng nó sẽ có nhu cầu rất cao vào năm 1995 khi mối quan tâm hàng đầu có lẽ là tốc độ của Java và thiếu tính tổng quát.
Doval 4/2/2015

7
@amon, một mô hình bảo mật đánh vào tôi như một lý do chính đáng để không thêm TCO vào ngôn ngữ đã có từ trước, nhưng một lý do kém là không thiết kế nó thành ngôn ngữ ngay từ đầu. Bạn không loại bỏ một tính năng hiển thị chính của lập trình viên để bao gồm một tính năng hậu trường nhỏ. "Tại sao Java 8 không có TCO" là một câu hỏi rất khác so với "Tại sao Java 1.0 không có TCO?" Tôi đã trả lời sau.
Karl Bielefeldt

3
@rwong, bạn có thể chuyển đổi bất kỳ hàm đệ quy nào thành hàm lặp. (Nếu tôi có thể, tôi đã viết một ví dụ ở đây )
alain

3
@ZanLynx nó phụ thuộc vào loại vấn đề. Ví dụ: Mẫu khách truy cập có thể hưởng lợi từ TCO (tùy thuộc vào chi tiết cụ thể về cấu trúc và khách truy cập) trong khi không liên quan gì đến FP. Đi qua các máy trạng thái là tương tự mặc dù việc chuyển đổi sử dụng trampolines có vẻ hơi tự nhiên hơn (tùy thuộc vào vấn đề). Ngoài ra, về mặt kỹ thuật, bạn có thể triển khai cây bằng các vòng lặp Tôi tin rằng sự đồng thuận là đệ quy tự nhiên hơn nhiều (tương tự đối với DFS mặc dù trong trường hợp này bạn có thể tránh đệ quy do giới hạn ngăn xếp ngay cả với TCO).
Maciej Piechotka

5

Java không có tối ưu hóa cuộc gọi cao vì JVM không có mã byte cho các cuộc gọi đuôi (đến một số con trỏ hàm không xác định tĩnh , ví dụ: một phương thức trong một số vtable).

Có vẻ như vì lý do xã hội (và có lẽ là kỹ thuật), việc thêm một hoạt động mã byte mới trong JVM (sẽ làm cho nó không tương thích với các phiên bản trước của JVM đó) là cực kỳ khó khăn đối với chủ sở hữu của đặc tả JVM.

Các lý do kỹ thuật cho việc không thêm mã byte mới trong đặc tả JVM bao gồm thực tế là việc triển khai JVM ngoài đời thực là những phần mềm phức tạp khủng khiếp (ví dụ vì có nhiều tối ưu hóa JIT mà nó đang thực hiện).

Các cuộc gọi đuôi đến một số hàm chưa biết yêu cầu thay thế khung ngăn xếp hiện tại bằng một khung mới và thao tác đó phải nằm trong JVM (đây không chỉ là vấn đề thay đổi trình biên dịch tạo mã byte).


17
Câu hỏi không phải là về các cuộc gọi đuôi, đó là về đệ quy đuôi. Và câu hỏi không phải là về ngôn ngữ mã byte JVM, đó là về ngôn ngữ lập trình Java, một ngôn ngữ hoàn toàn khác. Scala biên dịch thành mã byte JVM (trong số những thứ khác) và có loại bỏ đệ quy đuôi. Các triển khai lược đồ trên JVM có đầy đủ các cuộc gọi đuôi thích hợp. Java 7 (JVM Spec, 2nd Ed.) Đã thêm một mã byte mới. J9 JVM của IBM thực hiện TCO ngay cả khi không cần mã byte đặc biệt.
Jörg W Mittag

1
@ JörgWMittag: Điều đó đúng, nhưng sau đó, tôi tự hỏi liệu thực sự ngôn ngữ lập trình Java không có các cuộc gọi đuôi thích hợp hay không. Có thể chính xác hơn để nói rằng ngôn ngữ lập trình Java không cần phải có các cuộc gọi đuôi thích hợp, trong đó không có gì trong thông số kỹ thuật bắt buộc nó. (Đó là: Tôi không chắc chắn rằng bất cứ điều gì trong spec thực sự cấm một thực hiện từ cách loại bỏ các cuộc gọi đuôi Nó chỉ đơn giản là không được đề cập..)
ruakh

4

Trừ khi một ngôn ngữ có cú pháp đặc biệt để thực hiện cuộc gọi đuôi (đệ quy hoặc theo cách khác) và trình biên dịch sẽ vặn vẹo khi yêu cầu cuộc gọi đuôi nhưng không thể được tạo, tối ưu hóa cuộc gọi đuôi "tùy chọn" hoặc tối ưu hóa sẽ mang lại tình huống trong đó một đoạn mã có thể yêu cầu ít hơn 100 byte ngăn xếp trên một máy, nhưng hơn 100.000.000 byte của ngăn xếp trên máy khác. Một sự khác biệt như vậy nên được coi là định tính chứ không chỉ đơn thuần là định lượng.

Người ta hy vọng rằng các máy có thể có các kích thước ngăn xếp khác nhau và do đó, mã luôn có thể hoạt động trên một máy nhưng lại thổi chồng lên máy khác. Tuy nhiên, nói chung, mã sẽ hoạt động trên một máy ngay cả khi ngăn xếp bị hạn chế một cách giả tạo sẽ có khả năng hoạt động trên tất cả các máy có kích thước ngăn xếp "bình thường". Tuy nhiên, nếu một phương thức thu hồi sâu 1.000.000 được gọi là đuôi được tối ưu hóa trên một máy nhưng không phải là phương thức khác, thì thực thi trên máy cũ có thể sẽ hoạt động ngay cả khi ngăn xếp của nó nhỏ một cách bất thường và thất bại ngay cả khi ngăn xếp của nó lớn bất thường .


2

Tôi nghĩ rằng đệ quy cuộc gọi đuôi không được sử dụng trong Java chủ yếu vì làm như vậy sẽ thay đổi dấu vết ngăn xếp và do đó làm cho việc gỡ lỗi chương trình khó khăn hơn nhiều. Tôi nghĩ một trong những mục tiêu chính của Java là cho phép các lập trình viên dễ dàng gỡ lỗi mã của họ và theo dõi ngăn xếp là điều cần thiết để thực hiện điều đó đặc biệt là trong môi trường lập trình hướng đối tượng cao. Vì phép lặp có thể được sử dụng thay thế, ủy ban ngôn ngữ phải cho rằng nó không đáng để thêm đệ quy đuôi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.