Java casting có giới thiệu chi phí không? Tại sao?


103

Có bất kỳ chi phí nào khi chúng ta truyền các đối tượng thuộc loại này sang loại khác không? Hoặc trình biên dịch chỉ giải quyết mọi thứ và không có chi phí vào thời gian chạy?

Đây là những điều chung chung, hay có những trường hợp khác nhau?

Ví dụ, giả sử chúng ta có một mảng Đối tượng [], trong đó mỗi phần tử có thể có một kiểu khác nhau. Nhưng chúng ta luôn biết chắc chắn rằng phần tử 0 là một Double, phần tử 1 là một Chuỗi. (Tôi biết đây là một thiết kế sai, nhưng hãy cứ cho là tôi phải làm điều này.)

Thông tin kiểu của Java có còn được lưu giữ tại thời điểm chạy không? Hoặc mọi thứ đều bị lãng quên sau khi biên dịch và nếu chúng ta có các phần tử (Double) [0], chúng ta sẽ chỉ theo dõi con trỏ và giải thích 8 byte đó là một đôi, bất kể đó là gì?

Tôi rất không rõ về cách các kiểu được thực hiện trong Java. Nếu bạn có bất kỳ đề xuất nào về sách hoặc bài báo thì cũng xin cảm ơn.


Hiệu suất của instanceof và casting là khá tốt. Tôi đăng một số thời gian trong Java7 xung quanh cách tiếp cận khác nhau cho vấn đề ở đây: stackoverflow.com/questions/16320014/...
Wheezil

Câu hỏi khác này có câu trả lời rất tốt stackoverflow.com/questions/16741323/...
user454322

Câu trả lời:


77

Có 2 kiểu đúc:

Truyền ngầm , khi bạn truyền từ một loại sang một loại rộng hơn, được thực hiện tự động và không có chi phí:

String s = "Cast";
Object o = s; // implicit casting

Truyền rõ ràng , khi bạn chuyển từ loại rộng hơn sang loại hẹp hơn. Đối với trường hợp này, bạn phải sử dụng rõ ràng tính năng truyền như thế:

Object o = someObject;
String s = (String) o; // explicit casting

Trong trường hợp thứ hai này, có chi phí trong thời gian chạy, vì hai loại phải được kiểm tra và trong trường hợp việc truyền không khả thi, JVM phải ném một ClassCastException.

Lấy từ JavaWorld: Chi phí truyền

Đúc được sử dụng để chuyển đổi giữa các loại - cụ thể là giữa các loại tham chiếu, cho loại hoạt động đúc mà chúng tôi quan tâm ở đây.

Các phép toán nâng cấp (còn được gọi là mở rộng chuyển đổi trong Đặc tả ngôn ngữ Java) chuyển đổi một tham chiếu lớp con thành một tham chiếu lớp tổ tiên. Hoạt động truyền này thường tự động, vì nó luôn an toàn và có thể được trình biên dịch thực hiện trực tiếp.

Các hoạt động Downcast (còn được gọi là chuyển đổi thu hẹp trong Đặc tả ngôn ngữ Java) chuyển đổi một tham chiếu lớp tổ tiên thành một tham chiếu lớp con. Thao tác truyền này tạo ra chi phí thực thi, vì Java yêu cầu quá trình truyền được kiểm tra trong thời gian chạy để đảm bảo rằng nó hợp lệ. Nếu đối tượng được tham chiếu không phải là thể hiện của kiểu đích cho kiểu ép kiểu hoặc lớp con của kiểu đó, thì quá trình ép kiểu đã cố gắng không được phép và phải ném một java.lang.ClassCastException.


100
Bài báo JavaWorld đó đã hơn 10 năm tuổi, vì vậy tôi muốn lấy bất kỳ tuyên bố nào của nó về hiệu suất với một lượng rất lớn muối tốt nhất của bạn.
skaffman

@skaffman, Trên thực tế, tôi muốn đưa ra bất kỳ tuyên bố nào mà nó đưa ra (bất kể có liên quan đến hiệu suất không) với một chút muối.
Pacerier

sẽ là trường hợp tương tự, nếu tôi không gán đối tượng ép kiểu cho tham chiếu và chỉ gọi phương thức trên đó? thích((String)o).someMethodOfCastedClass()
Parth Vishvajit.

2
Bây giờ bài báo đã gần 20 năm tuổi. Và những câu trả lời cũng đã nhiều năm tuổi. Câu hỏi này cần một câu trả lời hiện đại.
Raslanove

Làm thế nào về các loại nguyên thủy? Tôi có nghĩa là, chẳng hạn - truyền từ int sang short gây ra chi phí tương tự?
luke1985,

44

Để triển khai Java một cách hợp lý:

Mỗi đối tượng có một tiêu đề chứa, trong số những thứ khác, một con trỏ đến kiểu thời gian chạy (ví dụ Doublehoặc String, nhưng nó không bao giờ có thể là CharSequencehoặc AbstractList). Giả sử trình biên dịch thời gian chạy (thường là HotSpot trong trường hợp của Sun) không thể xác định kiểu tĩnh, một số kiểm tra cần được thực hiện bởi mã máy được tạo.

Đầu tiên, con trỏ đến kiểu thời gian chạy cần được đọc. Điều này cũng cần thiết để gọi một phương thức ảo trong một tình huống tương tự.

Đối với truyền tới một loại lớp, bạn phải biết chính xác có bao nhiêu lớp cha cho đến khi bạn nhấn java.lang.Object, vì vậy, loại có thể được đọc ở độ lệch không đổi từ con trỏ loại (thực tế là tám lớp đầu tiên trong HotSpot). Một lần nữa, điều này tương tự như việc đọc một con trỏ phương thức cho một phương thức ảo.

Sau đó, giá trị đọc chỉ cần so sánh với kiểu tĩnh dự kiến ​​của ép kiểu. Tùy thuộc vào kiến ​​trúc tập lệnh, một lệnh khác sẽ cần phải phân nhánh (hoặc lỗi) trên một nhánh không chính xác. Các ISA như ARM 32-bit có chỉ dẫn có điều kiện và có thể có con đường buồn đi qua con đường hạnh phúc.

Các giao diện khó hơn do tính kế thừa nhiều giao diện. Nói chung, hai phôi cuối cùng đến giao diện được lưu trong bộ nhớ cache trong kiểu thời gian chạy. Vào những ngày đầu tiên (hơn một thập kỷ trước), các giao diện hơi chậm, nhưng điều đó không còn phù hợp nữa.

Hy vọng rằng bạn có thể thấy rằng loại điều này phần lớn không liên quan đến hiệu suất. Mã nguồn của bạn quan trọng hơn. Về mặt hiệu suất, tác động lớn nhất trong kịch bản của bạn là bộ nhớ cache bỏ lỡ việc theo đuổi các con trỏ đối tượng ở khắp nơi (tất nhiên thông tin loại sẽ là phổ biến).


1
thú vị - điều này có nghĩa là đối với các lớp không có giao diện nếu tôi viết lớp con Superclass sc = (Superclass); rằng trình biên dịch (jit tức là: thời gian tải) sẽ "tĩnh" đưa vào phần bù từ Đối tượng trong mỗi Lớp siêu và Lớp con trong tiêu đề "Lớp" của chúng và sau đó thông qua một phép cộng + so sánh đơn giản có thể giải quyết mọi thứ không? - đó là tốt và nhanh chóng :) Đối với giao diện, tôi sẽ cho rằng không tệ hơn một bảng băm nhỏ hoặc btree?
peterk

@peterk Để truyền giữa các lớp, cả địa chỉ đối tượng và "vtbl" (bảng con trỏ phương thức, cộng với bảng phân cấp lớp, bộ đệm giao diện, v.v.) không thay đổi. Vì vậy, [lớp] ép kiểu sẽ kiểm tra kiểu và nếu nó phù hợp thì không có gì khác xảy ra.
Tom Hawtin - tackline

8

Ví dụ, giả sử chúng ta có một mảng Đối tượng [], trong đó mỗi phần tử có thể có một kiểu khác nhau. Nhưng chúng ta luôn biết chắc chắn rằng phần tử 0 là một Double, phần tử 1 là một Chuỗi. (Tôi biết đây là một thiết kế sai, nhưng hãy cứ cho là tôi phải làm điều này.)

Trình biên dịch không lưu ý kiểu của các phần tử riêng lẻ của một mảng. Nó chỉ đơn giản là kiểm tra xem kiểu của mỗi biểu thức phần tử có thể gán cho kiểu phần tử mảng hay không.

Thông tin kiểu của Java có còn được lưu giữ tại thời điểm chạy không? Hoặc mọi thứ đều bị lãng quên sau khi biên dịch và nếu chúng ta có các phần tử (Double) [0], chúng ta sẽ chỉ theo dõi con trỏ và giải thích 8 byte đó là một đôi, bất kể đó là gì?

Một số thông tin được lưu giữ xung quanh thời gian chạy, nhưng không phải là kiểu tĩnh của các phần tử riêng lẻ. Bạn có thể biết điều này khi nhìn vào định dạng tệp lớp.

Về mặt lý thuyết, trình biên dịch JIT có thể sử dụng "phân tích thoát" để loại bỏ các kiểm tra kiểu không cần thiết trong một số bài tập. Tuy nhiên, làm điều này ở mức độ bạn đang đề xuất sẽ vượt quá giới hạn của tối ưu hóa thực tế. Lợi nhuận của việc phân tích các loại yếu tố riêng lẻ sẽ quá nhỏ.

Bên cạnh đó, dù sao mọi người cũng không nên viết mã ứng dụng như vậy.


1
Còn về nguyên thủy? (float) Math.toDegrees(theta)Sẽ có một khoản chi phí đáng kể ở đây?
SD

2
Có một chi phí cho một số phôi nguyên thủy. Nó có đáng kể hay không còn tùy thuộc vào bối cảnh.
Stephen C

6

Lệnh byte mã để thực hiện truyền trong thời gian chạy được gọi checkcast. Bạn có thể tháo rời mã Java bằng cách sử dụng javapđể xem những hướng dẫn nào được tạo.

Đối với mảng, Java giữ thông tin kiểu trong thời gian chạy. Hầu hết thời gian, trình biên dịch sẽ bắt lỗi kiểu cho bạn, nhưng có những trường hợp bạn sẽ gặp phải ArrayStoreExceptionkhi cố gắng lưu trữ một đối tượng trong một mảng, nhưng kiểu không khớp (và trình biên dịch không bắt được) . Đặc tả ngôn ngữ Java đưa ra ví dụ sau:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
    public static void main(String[] args) {
        ColoredPoint[] cpa = new ColoredPoint[10];
        Point[] pa = cpa;
        System.out.println(pa[1] == null);
        try {
            pa[0] = new Point();
        } catch (ArrayStoreException e) {
            System.out.println(e);
        }
    }
}

Point[] pa = cpalà hợp lệ vì ColoredPointlà một lớp con của Point, nhưng pa[0] = new Point()không hợp lệ.

Điều này trái ngược với các kiểu chung chung, nơi không có thông tin về kiểu được lưu giữ trong thời gian chạy. Trình biên dịch chèncheckcast hướng dẫn khi cần thiết.

Sự khác biệt trong cách nhập cho các kiểu và mảng chung này khiến việc trộn mảng và kiểu chung thường không phù hợp.


2

Về lý thuyết, có chi phí được giới thiệu. Tuy nhiên, các JVM hiện đại rất thông minh. Mỗi triển khai là khác nhau, nhưng không phải là không hợp lý khi cho rằng có thể tồn tại một triển khai mà JIT đã tối ưu hóa việc truyền kiểm tra khi nó có thể đảm bảo rằng sẽ không bao giờ có xung đột. Về việc các JVM cụ thể nào cung cấp điều này, tôi không thể cho bạn biết. Tôi phải thừa nhận rằng tôi muốn tự mình biết các chi tiết cụ thể của việc tối ưu hóa JIT, nhưng những điều này thì các kỹ sư JVM phải lo lắng.

Đạo đức của câu chuyện là viết mã dễ hiểu trước. Nếu bạn đang gặp sự cố chậm, hãy lập hồ sơ và xác định sự cố của bạn. Tỷ lệ cược tốt là nó sẽ không phải là do đúc. Đừng bao giờ hy sinh mã sạch, an toàn để cố gắng tối ưu hóa nó cho đến khi BẠN BIẾT BẠN CẦN LÀM.

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.