Tại sao JVM vẫn không hỗ trợ tối ưu hóa cuộc gọi đuôi?


95

Hai năm sau khi tối ưu hóa cuộc gọi-does-the-jvm-ngăn-đuôi , dường như có một triển khai nguyên mẫuMLVM đã liệt kê tính năng này là "proto 80%" trong một thời gian.

Có phải không có sự quan tâm tích cực nào từ phía Sun / Oracle trong việc hỗ trợ các lệnh gọi đuôi hay chỉ là các lệnh gọi đuôi "[...] định mệnh đứng ở vị trí thứ hai trong mọi danh sách ưu tiên tính năng [...]" như đã đề cập tại JVM Hội nghị thượng đỉnh ngôn ngữ ?

Tôi sẽ thực sự quan tâm nếu ai đó đã thử nghiệm bản dựng MLVM và có thể chia sẻ một số ấn tượng về cách nó hoạt động tốt (nếu có).

Cập nhật: Lưu ý rằng một số máy ảo như Avian hỗ trợ các cuộc gọi đuôi phù hợp mà không gặp bất kỳ sự cố nào.


14
Với sự di cư được báo cáo của những người Mặt trời khỏi Oracle, tôi sẽ không mong đợi rằng bất kỳ dự án nào hiện tại vẫn tiếp tục trừ khi Oracle nói rõ ràng như vậy :(
Thorbjørn Ravn Andersen

16
Lưu ý rằng câu trả lời được chấp nhận của bạn là hoàn toàn sai. Không có xung đột cơ bản giữa tối ưu hóa cuộc gọi đuôi và OOP và tất nhiên, một số ngôn ngữ như OCaml và F # có cả OOP và TCO.
JD

2
Chà, việc gọi các ngôn ngữ OCaml và F # OOP ngay từ đầu là một trò đùa dở. Nhưng có, OOP và TCO không có nhiều điểm chung, ngoại trừ thực tế là thời gian chạy phải kiểm tra xem phương pháp đang được tối ưu hóa không bị ghi đè / phân lớp ở một nơi khác.
soc

5
+1 Xuất thân từ nền tảng C, tôi luôn cho rằng TCO là một trong bất kỳ JVM hiện đại nào. Nó không bao giờ xảy ra với tôi để thực sự kiểm tra và khi tôi đã làm các kết quả đáng ngạc nhiên ...
thkala

2
@soc: "ngoại trừ thực tế là thời gian chạy phải kiểm tra xem phương thức đang được tối ưu hóa không bị ghi đè / phân lớp ở một nơi khác". "Sự thật" của bạn là hoàn toàn vô nghĩa.
JD

Câu trả lời:


32

Chẩn đoán mã Java: Cải thiện hiệu suất của mã Java của bạn ( alt ) giải thích lý do tại sao JVM không hỗ trợ tối ưu hóa cuộc gọi đuôi.

Nhưng mặc dù đã biết cách tự động biến đổi một hàm đệ quy đuôi thành một vòng lặp đơn giản, nhưng đặc tả Java không yêu cầu phải thực hiện chuyển đổi này. Có lẽ, một lý do khiến nó không phải là một yêu cầu là nói chung, việc chuyển đổi không thể được thực hiện một cách tĩnh trong một ngôn ngữ hướng đối tượng. Thay vào đó, việc chuyển đổi từ hàm đệ quy đuôi sang vòng lặp đơn giản phải được thực hiện động bởi trình biên dịch JIT.

Sau đó, nó đưa ra một ví dụ về mã Java sẽ không biến đổi.

Vì vậy, như ví dụ trong Liệt kê 3 cho thấy, chúng ta không thể mong đợi các trình biên dịch tĩnh thực hiện chuyển đổi đệ quy đuôi trên mã Java trong khi vẫn bảo toàn ngữ nghĩa của ngôn ngữ. Thay vào đó, chúng ta phải dựa vào biên dịch động của JIT. Tùy thuộc vào JVM, JIT có thể thực hiện hoặc không.

Sau đó, nó đưa ra một bài kiểm tra mà bạn có thể sử dụng để tìm hiểu xem JIT của bạn có thực hiện điều này hay không.

Đương nhiên, vì đây là một bài báo của IBM, nó bao gồm một phích cắm:

Tôi đã chạy chương trình này với một vài Java SDK và kết quả thật đáng ngạc nhiên. Chạy trên Hotspot JVM của Sun cho phiên bản 1.3 cho thấy rằng Hotspot không thực hiện chuyển đổi. Ở cài đặt mặc định, không gian ngăn xếp sẽ hết trong vòng chưa đầy một giây trên máy của tôi. Mặt khác, JVM của IBM cho phiên bản 1.3 chạy mà không có vấn đề gì, cho thấy rằng nó chuyển đổi mã theo cách này.


62
FWIW, các cuộc gọi đuôi không chỉ là về các hàm tự đệ quy như anh ấy ngụ ý. Lệnh gọi đuôi là bất kỳ lệnh gọi hàm nào xuất hiện ở vị trí đuôi. Chúng không phải là các cuộc gọi đến chính nó và chúng không phải là các cuộc gọi đến các vị trí tĩnh đã biết (ví dụ chúng có thể là các lệnh gọi phương thức ảo). Vấn đề mà anh ấy mô tả là không thành vấn đề nếu tối ưu hóa cuộc gọi đuôi được thực hiện đúng cách trong trường hợp chung và do đó, ví dụ của anh ấy hoạt động hoàn hảo trong các ngôn ngữ hướng đối tượng hỗ trợ lệnh gọi đuôi (ví dụ: OCaml và F #).
JD

3
"phải được thực hiện động bởi trình biên dịch JIT" có nghĩa là nó phải được thực hiện bởi chính JVM chứ không phải trình biên dịch Java. Nhưng OP đang hỏi về JVM.
Raedwald

11
"nói chung, việc chuyển đổi không thể được thực hiện tĩnh trong một ngôn ngữ hướng đối tượng." Tất nhiên, đây là một câu trích dẫn, nhưng mỗi khi tôi thấy lý do như vậy, tôi đều muốn hỏi về các con số - bởi vì tôi sẽ không ngạc nhiên nếu trong thực tế, trong phần lớn các trường hợp, nó có thể được thiết lập tại thời điểm biên dịch.
greenoldman

5
Liên kết đến bài báo được trích dẫn hiện đã bị hỏng, mặc dù Google đã lưu nó vào bộ nhớ đệm. Quan trọng hơn là cách lập luận của tác giả bị lỗi. Ví dụ được đưa ra có thể được tối ưu hóa cuộc gọi đuôi, sử dụng biên dịch tĩnh chứ không chỉ biên dịch động, nếu chỉ trình biên dịch chèn một instanceofkiểm tra để xem liệu thiscó phải là một Exampleđối tượng (chứ không phải là một lớp con của Example).
Alex D


30

Một lý do mà tôi đã thấy trước đây cho việc không triển khai TCO (và nó được coi là khó) trong Java là mô hình quyền trong JVM nhạy cảm với ngăn xếp và do đó, các cuộc gọi đuôi phải xử lý các khía cạnh bảo mật.

Tôi tin rằng điều này đã được Clements và Felleisen [1] [2] chứng minh không phải là một trở ngại và tôi khá chắc chắn rằng bản vá MLVM được đề cập trong câu hỏi cũng giải quyết được vấn đề đó.

Tôi nhận ra điều này không trả lời câu hỏi của bạn; chỉ thêm thông tin thú vị.

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

1
+1. Nghe câu hỏi / câu trả lời ở cuối bài thuyết trình này của Brian Goetz youtube.com/watch?v=2y5Pv4yN0b0&t=3739
mcoolive

15

Có lẽ bạn đã biết điều này rồi, nhưng tính năng này không quá tầm thường vì ngôn ngữ Java thực sự để lộ dấu vết ngăn xếp cho lập trình viên.

Hãy xem xét chương trình sau:

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());
    }
}

Mặc dù điều này có một "cuộc gọi đuôi", nó có thể không được tối ưu hóa. (Nếu nó được tối ưu hóa, nó vẫn yêu cầu lưu trữ toàn bộ call-stack vì ngữ nghĩa của chương trình dựa vào nó.)

Về cơ bản, điều này có nghĩa là rất khó để hỗ trợ điều này trong khi vẫn tương thích ngược.


17
Tìm thấy sai lầm trong suy nghĩ của bạn: "yêu cầu lưu giữ sách của toàn bộ ngăn xếp cuộc gọi vì ngữ nghĩa của chương trình dựa vào nó". :-) Nó giống như "Ngoại lệ bị đàn áp" mới. Các chương trình dựa vào những thứ như vậy nhất định bị phá vỡ. Theo tôi, hành vi của chương trình là hoàn toàn chính xác: Vứt bỏ các khung ngăn xếp là điều mà các cuộc gọi đuôi là tất cả về.
soc

4
@Marco, nhưng bất kỳ phương thức nào cũng có thể tạo ra một ngoại lệ, từ đó toàn bộ ngăn xếp cuộc gọi nhất định phải có sẵn, phải không? Bên cạnh đó, bạn không thể quyết định trước phương thức nào sẽ gọi gián tiếp gtrong trường hợp này ... hãy nghĩ về tính đa hình và phản xạ.
aioobe

2
Đó là một tác dụng phụ gây ra bởi việc bổ sung ARM trong Java 7. Đây là một ví dụ mà bạn không thể dựa vào những thứ như bạn đã trình bày ở trên.
soc

6
"thực tế là ngôn ngữ cho thấy ngăn xếp lời gọi làm cho nó khó thực hiện điều này": ngôn ngữ có yêu cầu rằng dấu vết ngăn xếp được trả về getStackTrace()từ một phương thức x()mà mã nguồn hiển thị được gọi từ một phương thức y()cũng cho thấy x()đã được gọi từ đó y()không? Trở thành nếu có một số tự do thì không có vấn đề thực sự.
Raedwald

8
Đây chỉ là vấn đề diễn đạt thông số kỹ thuật của một phương pháp duy nhất, từ "cung cấp cho bạn tất cả các khung ngăn xếp" đến "cung cấp cho bạn tất cả các khung xếp chồng đang hoạt động, loại bỏ những khung bị che khuất bởi các lệnh gọi đuôi". Hơn nữa, người ta có thể biến nó thành một công tắc dòng lệnh hoặc một thuộc tính hệ thống cho dù lệnh gọi đuôi có được thực hiện hay không.
Ingo

12

Java là ngôn ngữ ít chức năng nhất mà bạn có thể tưởng tượng (tốt, OK, có lẽ không !) Nhưng đây sẽ là một lợi thế lớn đối với các ngôn ngữ JVM, chẳng hạn như Scala .

Quan sát của tôi là việc biến JVM trở thành nền tảng cho các ngôn ngữ khác dường như chưa bao giờ đứng đầu danh sách ưu tiên đối với Sun và tôi đoán, bây giờ là đối với Oracle.


16
@ Thorbjørn - Tôi đã viết một chương trình để dự đoán liệu bất kỳ chương trình nhất định nào sẽ tạm dừng trong một khoảng thời gian hữu hạn. Tôi đã mất nhiều tuổi !
oxbow_lakes

3
Các BASIC đầu tiên tôi sử dụng không có chức năng, mà là GOSUB và RETURN. Tôi cũng không nghĩ LOLCODE có nhiều chức năng (và bạn có thể hiểu theo hai nghĩa).
David Thornley

1
@David, functions! = Có các chức năng.
Thorbjørn Ravn Andersen

2
@ Thorbjørn Ravn Andersen: Không, nhưng đó là điều kiện tiên quyết, bạn có nói không?
David Thornley

3
"Việc biến JVM trở thành nền tảng cho các ngôn ngữ khác dường như chưa bao giờ đứng đầu trong danh sách ưu tiên của Sun". Họ đã nỗ lực nhiều hơn đáng kể để biến JVM trở thành một nền tảng cho các ngôn ngữ động hơn so với các ngôn ngữ chức năng.
JD
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.