Các cảnh báo của việc thực hiện các loại cơ bản (như int) là các lớp là gì?


27

Khi thiết kế và implenting một ngôn ngữ lập trình hướng đối tượng, tại một số một thời điểm phải thực hiện một sự lựa chọn về việc thực hiện các loại cơ bản (như int, float, doublehoặc tương đương) như các lớp học hay cái gì khác. Rõ ràng, các ngôn ngữ trong họ C có xu hướng không định nghĩa chúng là các lớp (Java có các kiểu nguyên thủy đặc biệt, C # thực hiện chúng như các cấu trúc bất biến, v.v.).

Tôi có thể nghĩ về một lợi thế rất quan trọng khi các kiểu cơ bản được triển khai như các lớp (trong một hệ thống kiểu với hệ thống phân cấp thống nhất): các kiểu này có thể là các kiểu con Liskov thích hợp của kiểu gốc. Vì vậy, chúng tôi tránh làm phức tạp ngôn ngữ với quyền anh / unboxing (rõ ràng hoặc ẩn), các loại trình bao bọc, quy tắc phương sai đặc biệt, hành vi đặc biệt, v.v.

Tất nhiên, tôi có thể hiểu một phần lý do tại sao các nhà thiết kế ngôn ngữ quyết định cách họ làm: các thể hiện của lớp có xu hướng có một số chi phí không gian (vì các thể hiện có thể chứa một siêu dữ liệu vtable hoặc siêu dữ liệu khác trong bố cục bộ nhớ của họ), rằng các nguyên hàm / cấu trúc không cần có (nếu ngôn ngữ không cho phép thừa kế trên những cái đó).

Là hiệu quả không gian (và cải thiện không gian địa phương, đặc biệt là trong các mảng lớn) là lý do duy nhất tại sao các loại cơ bản thường không phải là các lớp?

Nói chung, tôi đã giả sử câu trả lời là có, nhưng trình biên dịch có thuật toán phân tích thoát và do đó họ có thể suy luận liệu họ có thể (chọn lọc) bỏ qua chi phí không gian hay không khi một trường hợp (bất kỳ trường hợp nào, không chỉ là loại cơ bản) được chứng minh là nghiêm ngặt địa phương.

Là sai ở trên, hoặc có cái gì khác tôi đang thiếu?


Câu trả lời:


19

Vâng, nó khá nhiều đến hiệu quả. Nhưng bạn dường như đang đánh giá thấp tác động (hoặc đánh giá quá cao mức độ tối ưu hóa khác nhau hoạt động).

Đầu tiên, nó không chỉ là "chi phí không gian". Làm cho nguyên thủy đóng hộp / phân bổ heap cũng có chi phí hiệu suất. Có thêm áp lực đối với GC để phân bổ và thu thập các đối tượng đó. Điều này gấp đôi nếu "các đối tượng nguyên thủy" là bất biến, như chúng phải vậy. Sau đó, có nhiều lỗi nhớ cache hơn (cả vì sự gián tiếp và vì ít dữ liệu phù hợp với một lượng bộ đệm nhất định). Cộng với thực tế trần trụi rằng "tải địa chỉ của một đối tượng, sau đó tải giá trị thực từ địa chỉ đó" cần nhiều hướng dẫn hơn "tải giá trị trực tiếp".

Thứ hai, phân tích thoát không phải là bụi cổ tích nhanh hơn. Nó chỉ áp dụng cho các giá trị mà, không, không thoát. Thật tuyệt khi tối ưu hóa các tính toán cục bộ (như bộ đếm vòng lặp và kết quả tính toán trung gian) và nó sẽ mang lại lợi ích có thể đo lường được. Nhưng phần lớn các giá trị sống trong các đối tượng và mảng lớn hơn nhiều. Cấp, những người đó có thể tự mình phân tích thoát, nhưng vì chúng thường là các loại tham chiếu có thể thay đổi, nên bất kỳ bí danh nào trong số chúng đều đưa ra một thách thức đáng kể cho phân tích thoát, giờ đây phải chứng minh rằng các bí danh (1) không thoát khỏi và (2) không tạo ra sự khác biệt cho mục đích loại bỏ phân bổ.

Cho rằng việc gọi bất kỳ phương thức nào (bao gồm cả getters) hoặc truyền một đối tượng làm đối số cho bất kỳ phương thức nào khác có thể giúp đối tượng thoát ra, bạn sẽ cần phân tích liên ngành trong tất cả các trường hợp ngoại trừ các trường hợp tầm thường nhất. Điều này là tốn kém và phức tạp hơn nhiều.

Và sau đó, có những trường hợp mọi thứ thực sự thoát ra và không thể được tối ưu hóa một cách hợp lý. Thực tế, khá nhiều trong số họ, nếu bạn xem xét mức độ thường xuyên mà các lập trình viên C phải trải qua những rắc rối của việc phân bổ đống. Khi một đối tượng chứa int thoát, phân tích thoát cũng ngừng áp dụng cho int. Nói lời tạm biệt với các lĩnh vực nguyên thủy hiệu quả .

Điều này liên quan đến một điểm khác: Các phân tích và tối ưu hóa cần thiết rất phức tạp và là một lĩnh vực nghiên cứu tích cực. Thật đáng tranh luận liệu có bất kỳ triển khai ngôn ngữ nào từng đạt được mức độ tối ưu hóa mà bạn đề xuất hay không, và ngay cả khi đó, đó là một nỗ lực hiếm hoi và mạnh mẽ. Chắc chắn đứng trên vai những người khổng lồ này dễ hơn là một người khổng lồ, nhưng nó vẫn còn xa tầm thường. Đừng mong đợi hiệu suất cạnh tranh bất cứ lúc nào trong vài năm đầu tiên, nếu có bao giờ.

Điều đó không có nghĩa là những ngôn ngữ như vậy không thể tồn tại. Rõ ràng là họ. Đừng cho rằng nó sẽ là dòng-line-line nhanh như ngôn ngữ với các nguyên thủy chuyên dụng. Nói cách khác, đừng ảo tưởng bản thân với tầm nhìn của một trình biên dịch đủ thông minh .


Khi nói về phân tích thoát, tôi cũng có nghĩa là phân bổ vào lưu trữ tự động (nó không giải quyết mọi thứ, nhưng như bạn nói, nó giải quyết một số điều). Tôi cũng thừa nhận rằng tôi đã đánh giá thấp mức độ mà các trường và bí danh có thể làm cho phân tích thoát thất bại thường xuyên hơn. Lỗi bộ nhớ cache là điều tôi quan tâm nhất khi nói về hiệu quả không gian, vì vậy cảm ơn bạn đã giải quyết điều đó.
Theodoros Chatzigiannakis

@TheodorosChatzigiannakis Tôi bao gồm thay đổi chiến lược phân bổ trong phân tích thoát (vì thực sự đó dường như là điều duy nhất nó từng được sử dụng cho).

Đoạn thứ hai của bạn: Các đối tượng không cần luôn được phân bổ heap hoặc là các kiểu tham chiếu. Trong thực tế, khi họ không, điều này làm cho việc tối ưu hóa cần thiết tương đối dễ dàng. Xem các đối tượng được phân bổ ngăn xếp của C ++ để biết ví dụ ban đầu và hệ thống sở hữu của Rust để biết cách phân tích thoát trực tiếp vào ngôn ngữ.
amon

@amon Tôi biết, và có lẽ tôi nên làm cho nó rõ ràng hơn, nhưng có vẻ như OP chỉ quan tâm đến các ngôn ngữ giống Java và C # trong đó phân bổ heap gần như là bắt buộc (và ẩn) vì ngữ nghĩa tham chiếu và các biểu thức không mất dữ liệu giữa các kiểu con. Điểm tốt về Rust sử dụng số tiền để thoát khỏi phân tích mặc dù!

@delnan Đúng là tôi hầu hết quan tâm đến các ngôn ngữ trừu tượng hóa các chi tiết lưu trữ, nhưng xin vui lòng bao gồm mọi thứ bạn nghĩ là có liên quan, ngay cả khi nó không áp dụng được trong các ngôn ngữ đó.
Theodoros Chatzigiannakis

27

Là hiệu quả không gian (và cải thiện không gian địa phương, đặc biệt là trong các mảng lớn) là lý do duy nhất tại sao các loại cơ bản thường không phải là các lớp?

Không.

Vấn đề khác là các loại cơ bản có xu hướng được sử dụng bởi các hoạt động cơ bản. Trình biên dịch cần biết rằng int + intsẽ không được biên dịch thành một lệnh gọi hàm, mà là một số lệnh CPU cơ bản (hoặc mã byte tương đương). Tại thời điểm đó, nếu bạn có intmột đối tượng bình thường, bạn sẽ phải hủy hộp thư một cách hiệu quả.

Những loại hoạt động đó cũng không thực sự chơi tốt với phân nhóm. Bạn không thể gửi đến một hướng dẫn CPU. Bạn không thể gửi từ một hướng dẫn CPU. Tôi có nghĩa là toàn bộ điểm của phân nhóm là để bạn có thể sử dụng một Dnơi bạn có thể a B. Hướng dẫn CPU không đa hình. Để có được các nguyên thủy để làm điều đó, bạn phải bọc các hoạt động của chúng bằng logic điều phối có chi phí gấp nhiều lần số lượng hoạt động như là phép cộng đơn giản (hoặc bất cứ điều gì). Lợi ích của việc inttrở thành một phần của hệ thống phân cấp kiểu sẽ trở thành một chút ít khi nó được niêm phong / cuối cùng. Và đó là bỏ qua tất cả các vấn đề đau đầu với logic gửi cho các nhà khai thác nhị phân ...

Về cơ bản, các kiểu nguyên thủy sẽ cần có nhiều quy tắc đặc biệt xung quanh cách trình biên dịch xử lý chúng và dù sao người dùng cũng có thể làm gì với các kiểu của chúng , do đó, thường đơn giản hơn khi chỉ coi chúng là hoàn toàn khác biệt.


4
Kiểm tra việc thực hiện bất kỳ ngôn ngữ được gõ động nào xử lý các số nguyên và chẳng hạn như các đối tượng. Lệnh CPU nguyên thủy cuối cùng rất có thể được ẩn trong một phương thức (quá tải toán tử) trong việc thực hiện lớp chỉ có một chút đặc quyền trong thư viện thời gian chạy. Các chi tiết sẽ khác nhau với một hệ thống và trình biên dịch kiểu tĩnh nhưng nó không phải là vấn đề cơ bản. Tồi tệ nhất nó chỉ làm cho mọi thứ thậm chí chậm hơn.

3
int + intcó thể là một toán tử mức ngôn ngữ thông thường gọi ra một lệnh nội tại được đảm bảo để biên dịch thành (hoặc hành xử như) bổ sung số nguyên CPU gốc op. Lợi ích của việc intthừa hưởng từ objectkhông chỉ là khả năng thừa hưởng một loại khác int, mà còn là khả năng intcư xử như một người objectkhông có quyền anh. Hãy xem xét tổng quát C #: bạn có thể có hiệp phương sai và chống chỉ định, nhưng chúng chỉ có thể áp dụng cho các loại lớp - các kiểu cấu trúc được tự động loại trừ, bởi vì chúng chỉ có thể trở thành quyền anh objectthông qua (ngầm định, do trình biên dịch tạo ra).
Theodoros Chatzigiannakis

3
@delnan - chắc chắn, mặc dù theo kinh nghiệm của tôi với việc triển khai gõ tĩnh, vì mọi cuộc gọi phi hệ thống đều tập trung vào các hoạt động nguyên thủy, có chi phí hoạt động mạnh mẽ - do đó có tác động mạnh mẽ hơn đến việc áp dụng.
Telastyn

@TheodorosChatzigiannakis - thật tuyệt, vì vậy bạn có thể nhận được phương sai và chống chỉ định đối với các loại không có loại phụ / siêu hữu ích ... Và việc triển khai toán tử đặc biệt đó để gọi lệnh CPU vẫn khiến nó trở nên đặc biệt. Tôi không đồng ý với ý kiến ​​này - Tôi đã thực hiện những điều rất giống nhau trong ngôn ngữ đồ chơi của mình, nhưng tôi thấy rằng có những vấn đề thực tế trong quá trình thực hiện mà không làm cho mọi thứ trở nên sạch sẽ như bạn mong đợi.
Telastyn

1
@TheodorosChatzigiannakis Đặt nội tuyến qua ranh giới thư viện là điều hoàn toàn có thể, mặc dù đó là một mục khác trong danh sách mua sắm "tối ưu hóa cao cấp mà tôi muốn có". Tôi cảm thấy bắt buộc phải chỉ ra rằng mặc dù nó nổi tiếng là khó khăn để hoàn toàn đúng mà không quá bảo thủ đến mức vô dụng.

4

Chỉ có rất ít trường hợp bạn cần các loại cơ bản của cơ sở dữ liệu, đó là các đối tượng đầy đủ (ở đây, một đối tượng là dữ liệu chứa con trỏ tới cơ chế điều phối hoặc được gắn thẻ với loại có thể được sử dụng bởi cơ chế điều phối):

  • Bạn muốn các loại do người dùng định nghĩa có thể kế thừa từ các loại cơ bản. Điều này thường không muốn vì nó giới thiệu các vấn đề đau đầu liên quan đến hiệu suất và bảo mật. Đây là một vấn đề về hiệu năng vì quá trình biên dịch không thể giả định rằng intsẽ có một kích thước cố định cụ thể hoặc không có phương thức nào bị ghi đè và đó là một vấn đề bảo mật vì ngữ nghĩa của ints có thể bị phá vỡ (xem xét một số nguyên bằng bất kỳ số nào, hoặc mà thay đổi giá trị của nó chứ không phải là bất biến).

  • Các kiểu nguyên thủy của bạn có siêu kiểu và bạn muốn có các biến với kiểu siêu kiểu của kiểu nguyên thủy. Ví dụ: giả sử ints của bạn là Hashablevà bạn muốn khai báo một hàm lấy Hashabletham số có thể nhận các đối tượng thông thường nhưng cũng ints.

    Điều này có thể được giải quyết trên mạng bằng cách biến các loại đó thành bất hợp pháp: loại bỏ phân nhóm và quyết định rằng các giao diện không phải là loại mà là các ràng buộc kiểu. Rõ ràng điều đó làm giảm tính biểu cảm của hệ thống loại của bạn và một hệ thống loại như vậy sẽ không còn được gọi là hướng đối tượng nữa. Xem Haskell cho một ngôn ngữ sử dụng chiến lược này. C ++ cách đó một nửa vì các kiểu nguyên thủy không có siêu kiểu.

    Sự thay thế là quyền anh toàn phần hoặc một phần của các loại cơ bản. Các loại quyền anh không cần phải nhìn thấy người dùng. Về cơ bản, bạn xác định loại đóng hộp nội bộ cho từng loại cơ bản và chuyển đổi ngầm giữa loại được đóng hộp và loại cơ bản. Điều này có thể gây khó xử nếu các loại đóng hộp có ngữ nghĩa khác nhau. Java thể hiện hai vấn đề: các kiểu đóng hộp có khái niệm về danh tính trong khi các kiểu nguyên thủy chỉ có khái niệm tương đương giá trị và các kiểu đóng hộp là không thể trong khi các kiểu nguyên thủy luôn có giá trị. Các vấn đề này hoàn toàn có thể tránh được bằng cách không đưa ra khái niệm nhận dạng cho các loại giá trị, cung cấp quá tải toán tử và không làm cho tất cả các đối tượng trở nên vô hiệu theo mặc định.

  • Bạn không có tính năng gõ tĩnh. Một biến có thể chứa bất kỳ giá trị nào, bao gồm các kiểu hoặc đối tượng nguyên thủy. Do đó, tất cả các loại nguyên thủy cần phải luôn được đóng hộp để đảm bảo gõ mạnh.

Các ngôn ngữ có kiểu gõ tĩnh thực hiện tốt việc sử dụng các kiểu nguyên thủy bất cứ khi nào có thể và chỉ quay lại các kiểu được đóng hộp như là phương sách cuối cùng. Mặc dù nhiều chương trình không nhạy cảm về hiệu năng, nhưng có những trường hợp kích thước và kiểu trang điểm nguyên thủy cực kỳ phù hợp: Hãy nghĩ đến việc bẻ khóa quy mô lớn trong đó bạn cần lắp hàng tỷ điểm dữ liệu vào bộ nhớ. Chuyển từ doublesangfloatcó thể là một chiến lược tối ưu hóa không gian khả thi trong C, nhưng nó sẽ không có tác dụng nếu tất cả các loại số luôn được đóng hộp (và do đó lãng phí ít nhất một nửa bộ nhớ của chúng cho một con trỏ cơ chế điều phối). Khi các kiểu nguyên thủy được đóng hộp được sử dụng cục bộ, việc loại bỏ quyền anh thông qua việc sử dụng nội tại của trình biên dịch là khá đơn giản, nhưng sẽ rất thiển cận khi đặt cược hiệu suất tổng thể của ngôn ngữ của bạn vào một trình biên dịch nâng cao đầy đủ.


An intlà hầu như không thay đổi trong tất cả các ngôn ngữ.
Scott Whitlock

6
@ScottWhitlock Tôi thấy lý do tại sao bạn có thể nghĩ như vậy, nhưng nói chung các loại nguyên thủy là loại giá trị bất biến. Không có ngôn ngữ lành mạnh cho phép bạn thay đổi giá trị của số bảy. Tuy nhiên, nhiều ngôn ngữ cho phép bạn gán lại một biến chứa giá trị của kiểu nguyên thủy thành giá trị khác. Trong các ngôn ngữ giống như C, một biến là một vị trí bộ nhớ được đặt tên và hoạt động như một con trỏ. Một biến không giống với giá trị mà nó trỏ đến. Một intgiá trị là bất biến, nhưng một intbiến không.
amon

1
@amon: Không có ngôn ngữ lành mạnh; chỉ cần Java: thed Dailywtf.com/articles/Disgruntled-Bomb-Java-Edition
Mason Wheeler

get rid of subtyping and decide that interfaces aren't types but type constraints.... such a type system wouldn't be called object-oriented any longer nhưng điều này nghe giống như lập trình dựa trên nguyên mẫu, chắc chắn là OOP.
Michael

1
@ScottWhitlock câu hỏi là nếu sau đó bạn có int b = a, bạn có thể làm gì đó để b sẽ thay đổi giá trị của a. Đã có một số triển khai ngôn ngữ trong đó điều này là có thể, nhưng nó thường được coi là bệnh hoạn và không mong muốn, không giống như làm tương tự cho một mảng.
Random832

2

Hầu hết các triển khai tôi nhận thấy áp đặt ba hạn chế đối với các lớp như vậy cho phép trình biên dịch sử dụng hiệu quả các kiểu nguyên thủy như đại diện cơ bản trong phần lớn thời gian. Những hạn chế này là:

  • Bất biến
  • Tài chính (không thể bắt nguồn từ)
  • Gõ tĩnh

Các tình huống trong đó một trình biên dịch cần đóng hộp nguyên thủy vào một đối tượng trong biểu diễn bên dưới là tương đối hiếm, chẳng hạn như khi một Objecttham chiếu đang trỏ đến nó.

Điều này thêm một chút xử lý trường hợp đặc biệt trong trình biên dịch, nhưng nó không chỉ giới hạn ở một số trình biên dịch siêu tiên tiến huyền thoại. Tối ưu hóa đó là trong trình biên dịch sản xuất thực trong các ngôn ngữ chính. Scala thậm chí cho phép bạn xác định các lớp giá trị của riêng bạn.


1

Trong Smalltalk tất cả chúng (int, float, v.v.) là các đối tượng hạng nhất. Các chỉ trường hợp đặc biệt là SmallIntegers được hệ thống hóa và xử khác biệt của Virtual Machine vì lợi ích của hiệu quả, và do đó lớp SmallInteger sẽ không thừa nhận các lớp con (mà không phải là một giới hạn thực tế.) Lưu ý rằng điều này không đòi hỏi bất kỳ quan tâm đặc biệt về phía người lập trình vì sự khác biệt được đặt vào các thói quen tự động như tạo mã hoặc thu gom rác.

Cả Trình biên dịch Smalltalk (mã nguồn -> mã byte VM) và trình tạo ảo VM (mã byte -> mã máy) tối ưu hóa mã được tạo (JIT) để giảm hình phạt của các hoạt động cơ bản với các đối tượng cơ bản này.


1

Tôi đã thiết kế một ngôn ngữ và thời gian chạy OO (điều này không thành công vì một lý do hoàn toàn khác).

Không có gì sai khi tạo ra những thứ như int true class; trong thực tế, điều này làm cho GC dễ dàng thiết kế hơn vì hiện tại chỉ có 2 loại tiêu đề heap (lớp & mảng) thay vì 3 (lớp, mảng và nguyên thủy) [thực tế là chúng ta có thể hợp nhất lớp & mảng sau khi điều này không liên quan ].

Trường hợp thực sự quan trọng, các kiểu nguyên thủy nên có các phương thức cuối cùng / kín (+ thực sự quan trọng, ToString không quá nhiều). Điều này cho phép trình biên dịch giải quyết tĩnh hầu hết tất cả các cuộc gọi đến chính các hàm và nội tuyến chúng. Trong hầu hết các trường hợp, điều này không quan trọng bằng hành vi sao chép (tôi đã chọn để nhúng khả dụng ở cấp ngôn ngữ [.NET cũng vậy)), nhưng trong một số trường hợp nếu các phương thức không được niêm phong, trình biên dịch sẽ buộc phải tạo cuộc gọi đến hàm được sử dụng để thực hiện int + int.

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.