Liệu hướng đối tượng có thực sự ảnh hưởng đến hiệu suất thuật toán?


14

Định hướng đối tượng đã giúp tôi rất nhiều trong việc thực hiện nhiều thuật toán. Tuy nhiên, các ngôn ngữ hướng đối tượng đôi khi hướng dẫn bạn theo cách tiếp cận "đơn giản" và tôi nghi ngờ liệu phương pháp này luôn luôn là một điều tốt.

OO thực sự hữu ích trong các thuật toán mã hóa nhanh chóng và dễ dàng. Nhưng OOP này có thể là một bất lợi cho phần mềm dựa trên hiệu suất, tức là chương trình thực thi nhanh như thế nào?

Ví dụ, việc lưu trữ các nút biểu đồ trong cấu trúc dữ liệu có vẻ "đơn giản" ngay từ đầu, nhưng nếu các đối tượng Node chứa nhiều thuộc tính và phương thức, điều này có thể dẫn đến thuật toán chậm không?

Nói cách khác, nhiều tham chiếu giữa nhiều đối tượng khác nhau, hoặc sử dụng nhiều phương thức từ nhiều lớp, có thể dẫn đến việc thực hiện "nặng" không?


1
Một câu hỏi khá lạ. Tôi có thể hiểu làm thế nào OOP giúp về mức độ kiến ​​trúc. Nhưng một mức độ triển khai thuật toán thường được xây dựng dựa trên sự trừu tượng rất xa lạ với bất cứ điều gì OOP đại diện. Vì vậy, rất có thể, hiệu suất không phải là vấn đề lớn nhất đối với việc triển khai thuật toán OOP của bạn. Về hiệu năng, với OOP, nút cổ chai lớn nhất thường liên quan đến các cuộc gọi ảo.
SK-logic

@ SK-logic> hướng đối tượng có xu hướng thao túng con trỏ bằng con trỏ, nghĩa là khối lượng công việc quan trọng hơn ở phía cấp phát bộ nhớ và dữ liệu không được bản địa hóa có xu hướng không nằm trong bộ đệm CPU và cuối cùng nhưng không kém phần quan trọng, ngụ ý rất nhiều gián tiếp phân nhánh (chức năng ảo) gây chết người cho đường ống CPU. OO là một điều tốt, nhưng nó chắc chắn có thể có chi phí hiệu suất trong một số trường hợp.
deadalnix

Nếu các nút trong biểu đồ của bạn có hàng trăm thuộc tính, bạn sẽ cần nơi lưu trữ chúng bất kể mô hình được sử dụng để triển khai thực tế và tôi không thấy bất kỳ mô hình đơn lẻ nào có lợi thế chung về điều này. @deadalnix: Có thể các yếu tố liên tục có thể tồi tệ hơn do làm cho việc tối ưu hóa nhất định trở nên khó khăn hơn. Nhưng lưu ý rằng tôi nói khó hơn , không phải là không thể - ví dụ, PyPy có thể hủy hộp các đối tượng trong các vòng lặp chặt chẽ và các JVM đã thực hiện các lệnh gọi hàm ảo từ mãi mãi.

Python tốt cho các thuật toán tạo mẫu, nhưng bạn thường không cần một lớp khi thực hiện một thuật toán điển hình trong nó.
Công việc

1
+1 Đối với định hướng đối tượng liên quan với các thuật toán, một cái gì đó bị bỏ qua những ngày này, cả trong ngành công nghiệp phần mềm và học viện ...
umlcat

Câu trả lời:


16

Định hướng đối tượng có thể ngăn chặn một số tối ưu hóa thuật toán nhất định, vì đóng gói. Hai thuật toán có thể hoạt động đặc biệt tốt với nhau, nhưng nếu chúng bị ẩn sau các giao diện OO, khả năng sử dụng sức mạnh tổng hợp của chúng sẽ bị mất.

Nhìn vào các thư viện số. Rất nhiều trong số họ (không chỉ những người được viết trong thập niên 60 hoặc 70) không phải là OOP. Có một lý do cho điều đó - thuật toán số hoạt động tốt hơn như một tập hợp tách rời moduleshơn là phân cấp OO với giao diện và đóng gói.


2
Lý do chính cho điều đó là chỉ C ++ đã tìm ra cách sử dụng các mẫu biểu thức để làm cho phiên bản OO có hiệu quả như vậy.
DeadMG

4
Nhìn vào các thư viện C ++ hiện đại (STL, Boost) - chúng cũng không phải là OOP. Và không chỉ vì hiệu suất. Các thuật toán thường không thể được biểu diễn tốt theo kiểu OOP. Những thứ như lập trình chung phù hợp hơn nhiều cho các thuật toán cấp thấp.
SK-logic

3
Cái gì-cái gì? Tôi đoán tôi đến từ một hành tinh khác với quant_dev và SK-logic. Không, một vũ trụ khác. Với các định luật vật lý khác nhau và mọi thứ.
Mike Nakis

5
@MikeNakis: sự khác biệt về quan điểm nằm ở (1) liệu một đoạn mã tính toán nhất định có thể có lợi về khả năng đọc của con người từ OOP hay không (mà công thức số không có); (2) liệu thiết kế lớp OOP có phù hợp với cấu trúc dữ liệu và thuật toán tối ưu hay không (xem câu trả lời của tôi); và (3) liệu mỗi lớp của chỉ định có cung cấp đủ "giá trị" hay không (về mặt công việc được thực hiện cho mỗi lệnh gọi hàm hoặc độ rõ ràng về khái niệm trên mỗi lớp) chỉ ra chi phí (do lệnh gián tiếp, gọi hàm, lớp hoặc sao chép dữ liệu). (4) Cuối cùng, sự tinh vi của trình biên dịch / JIT / trình tối ưu hóa là yếu tố hạn chế.
rwong

2
@MikeNakis, ý bạn là gì? Bạn có nghĩ rằng STL là một thư viện OOP? Lập trình chung không phù hợp với OOP. Và không cần phải đề cập rằng OOP là một khung quá hẹp, chỉ phù hợp cho một vài nhiệm vụ thực tế, xa lạ cho bất cứ điều gì khác.
SK-logic

9

Điều gì quyết định hiệu suất?

Các nguyên tắc cơ bản: cấu trúc dữ liệu, thuật toán, kiến ​​trúc máy tính, phần cứng. Cộng thêm chi phí.

Một chương trình OOP có thể được thiết kế để căn chỉnh chính xác với sự lựa chọn cấu trúc dữ liệu và thuật toán được coi là tối ưu theo lý thuyết CS. Nó sẽ có đặc tính hiệu suất tương tự như chương trình tối ưu, cộng với một số chi phí. Chi phí thường có thể được giảm thiểu.

Tuy nhiên, một chương trình ban đầu được thiết kế chỉ có mối quan tâm OOP, không liên quan đến các nguyên tắc cơ bản, ban đầu có thể không tối ưu. Sự tối ưu phụ đôi khi có thể tháo rời bằng cách tái cấu trúc; đôi khi nó không - đòi hỏi phải viết lại hoàn toàn.

Hãy cẩn thận: hiệu suất trong vấn đề phần mềm kinh doanh?

Có, nhưng thời gian tiếp thị (TTM) quan trọng hơn, theo đơn đặt hàng lớn. Phần mềm kinh doanh nhấn mạnh vào khả năng thích ứng của mã với các quy tắc kinh doanh phức tạp. Các phép đo hiệu suất nên được thực hiện trong suốt vòng đời phát triển. (Xem phần: hiệu suất tối ưu có nghĩa là gì? ) Chỉ nên cải tiến thị trường và nên được giới thiệu dần dần trong các phiên bản sau.

Hiệu suất tối ưu có nghĩa là gì?

Nói chung, vấn đề với hiệu suất phần mềm là: để chứng minh rằng "phiên bản nhanh hơn tồn tại", phiên bản nhanh hơn đó phải xuất hiện trước tiên (tức là không có bằng chứng nào khác ngoài chính nó).

Đôi khi phiên bản nhanh hơn đó lần đầu tiên được nhìn thấy trong một ngôn ngữ hoặc mô hình khác. Điều này nên được coi là một gợi ý để cải thiện, không phải là một đánh giá về sự thấp kém của một số ngôn ngữ hoặc mô hình khác.

Tại sao chúng tôi thực hiện OOP nếu nó có thể cản trở tìm kiếm của chúng tôi cho hiệu suất tối ưu?

OOP giới thiệu chi phí hoạt động (trong không gian và thực thi), để đổi lấy việc cải thiện "khả năng làm việc" và do đó giá trị kinh doanh của mã. Điều này làm giảm chi phí phát triển và tối ưu hóa hơn nữa. Xem @MikeNakis .

Những phần nào của OOP có thể khuyến khích một thiết kế phụ tối ưu ban đầu?

Các phần của OOP mà (i) khuyến khích tính đơn giản / trực giác, (ii) sử dụng các phương pháp thiết kế thông tục thay vì các nguyên tắc cơ bản, (iii) không khuyến khích nhiều triển khai phù hợp cho cùng một mục đích.

  • HÔN
  • YAGNI
  • KHÔ
  • Thiết kế đối tượng (ví dụ với thẻ CRC) mà không đưa ra những suy nghĩ bình đẳng cho các nguyên tắc cơ bản)

Áp dụng nghiêm ngặt một số nguyên tắc OOP (đóng gói, chuyển thông điệp, làm tốt một việc) thực sự sẽ dẫn đến mã chậm hơn lúc đầu. Đo lường hiệu suất sẽ giúp chẩn đoán những vấn đề. Miễn là cấu trúc dữ liệu và thuật toán phù hợp với thiết kế tối ưu dự đoán theo lý thuyết, chi phí thường có thể được giảm thiểu.

Các giảm thiểu phổ biến đối với tổng phí OOP là gì?

Như đã nêu trước đây, sử dụng các cấu trúc dữ liệu tối ưu cho thiết kế.

Một số ngôn ngữ hỗ trợ mã nội tuyến có thể phục hồi một số hiệu năng thời gian chạy.

Làm thế nào chúng ta có thể áp dụng OOP mà không phải hy sinh hiệu suất?

Tìm hiểu và áp dụng cả OOP và các nguyên tắc cơ bản.

Đúng là việc tuân thủ nghiêm ngặt OOP có thể ngăn bạn viết một phiên bản nhanh hơn. Đôi khi một phiên bản nhanh hơn chỉ có thể được viết từ đầu. Đây là lý do tại sao nó giúp viết nhiều phiên bản mã bằng các thuật toán và mô hình khác nhau (OOP, chung, chức năng, toán học, spaghetti), sau đó sử dụng các công cụ tối ưu hóa để làm cho mỗi phiên bản tiếp cận hiệu suất tối đa quan sát được.

Có loại mã nào không được hưởng lợi từ OOP không?

(Được mở rộng từ cuộc thảo luận giữa [@quant_dev], [@ SK-logic] và [@MikeNakis])

  1. Công thức nấu ăn số, bắt nguồn từ toán học.
    • Các phương trình toán học và tự biến đổi có thể được hiểu là các đối tượng.
    • Các kỹ thuật chuyển đổi mã rất tinh vi là cần thiết để tạo mã thực thi hiệu quả. Việc thực hiện ngây thơ ("bảng trắng") sẽ có hiệu suất rất cao.
    • Tuy nhiên, trình biên dịch chính ngày nay không thể làm như vậy.
    • Phần mềm chuyên dụng (MATLAB và Mathicala, v.v.) có cả JIT và bộ giải biểu tượng có thể tạo mã hiệu quả cho một số vấn đề phụ. Các bộ giải chuyên biệt này có thể được xem như là các trình biên dịch có mục đích đặc biệt (các trung gian giữa mã có thể đọc được của con người và mã có thể thực thi bằng máy), chúng sẽ được hưởng lợi từ thiết kế OOP.
    • Mỗi vấn đề phụ đòi hỏi "trình biên dịch" và "biến đổi mã" riêng. Do đó, đây là một lĩnh vực nghiên cứu mở rất tích cực với kết quả mới xuất hiện hàng năm.
    • Bởi vì nghiên cứu mất nhiều thời gian, các nhà văn phần mềm phải thực hiện tối ưu hóa trên giấy và sao chép mã được tối ưu hóa thành phần mềm. Mã được phiên mã có thể thực sự không thể hiểu được.
  2. Mã cấp rất thấp.
      *

8

Đó không thực sự là về định hướng đối tượng như về container. Nếu bạn đã sử dụng danh sách liên kết kép để lưu trữ pixel trong trình phát video của mình thì điều đó sẽ bị ảnh hưởng.

Tuy nhiên, nếu bạn sử dụng đúng thùng chứa, không có lý do gì std :: vector chậm hơn một mảng và vì bạn có tất cả các thuật toán phổ biến đã được viết cho nó - bởi các chuyên gia - nó có thể nhanh hơn mã mảng cuộn của bạn.


1
Bởi vì trình biên dịch là tối ưu phụ (hoặc các quy tắc của ngôn ngữ lập trình cấm sử dụng các giả định hoặc tối ưu hóa nhất định), nên thực sự có một chi phí không thể loại bỏ. Ngoài ra, một số tối ưu hóa nhất định, ví dụ như vector hóa có các yêu cầu tổ chức dữ liệu (ví dụ: cấu trúc của mảng thay vì mảng cấu trúc) mà OOP có thể tăng cường hoặc cản trở. (Gần đây tôi chỉ làm việc với một nhiệm vụ tối ưu hóa vector std ::.)
rwong

5

OOP rõ ràng là một ý tưởng tốt, và giống như bất kỳ ý tưởng tốt nào, nó có thể được sử dụng quá mức. Theo kinh nghiệm của tôi, nó là cách sử dụng quá mức. Hiệu suất kém và kết quả bảo trì kém.

Nó không liên quan gì đến việc gọi các hàm ảo và không liên quan nhiều đến những gì trình tối ưu hóa / jitter làm.

Nó có mọi thứ để làm với các cấu trúc dữ liệu, trong khi có hiệu suất big-O tốt nhất, có các yếu tố hằng số rất xấu. Điều này được thực hiện dựa trên giả định rằng nếu có bất kỳ vấn đề hạn chế hiệu năng nào trong ứng dụng, thì đó là vấn đề khác.

Một cách mà biểu hiện này là số lần mỗi giây mới được thực hiện, được cho là có hiệu suất O (1), nhưng có thể thực hiện hàng trăm đến hàng nghìn hướng dẫn (bao gồm cả thời gian xóa phù hợp hoặc thời gian GC). Điều đó có thể được giảm thiểu bằng cách lưu các đối tượng đã sử dụng, nhưng điều đó làm cho mã ít "sạch" hơn.

Một cách khác mà nó thể hiện là cách mọi người được khuyến khích viết các hàm thuộc tính, xử lý thông báo, gọi đến các hàm lớp cơ sở, tất cả các loại lệnh gọi hàm ngầm tồn tại để cố gắng duy trì tính nhất quán. Để duy trì tính nhất quán, họ có thành công hạn chế, nhưng họ cực kỳ thành công khi lãng phí chu kỳ. Các lập trình viên hiểu khái niệm dữ liệu chuẩn hóa nhưng họ có xu hướng chỉ áp dụng nó cho thiết kế cơ sở dữ liệu. Họ không áp dụng nó cho thiết kế cấu trúc dữ liệu, ít nhất một phần vì OOP nói với họ rằng họ không phải làm vậy. Một điều đơn giản như việc thiết lập bit Modified trong một đối tượng có thể dẫn đến một cơn sóng thần cập nhật chạy qua cấu trúc dữ liệu, bởi vì không có lớp nào xứng đáng với mã của nó nhận một cuộc gọi được sửa đổi và chỉ lưu trữ nó.

Có thể hiệu suất của một ứng dụng nhất định chỉ tốt như được viết.

Mặt khác, nếu có vấn đề về hiệu năng, đây là một ví dụ về cách tôi điều chỉnh nó. Đó là một quá trình nhiều giai đoạn. Ở mỗi giai đoạn, một số hoạt động cụ thể chiếm một phần lớn thời gian và có thể được thay thế bằng một cái gì đó nhanh hơn. (Tôi không nói "nút cổ chai". Đây không phải là những thứ mà trình biên dịch giỏi tìm kiếm.) Quá trình này thường yêu cầu, để có được sự tăng tốc, thay thế cấu trúc dữ liệu bán buôn. Thường thì cấu trúc dữ liệu đó chỉ có ở đó vì nó được khuyến nghị thực hành OOP.


3

Về lý thuyết, nó có thể dẫn đến sự chậm chạp, nhưng ngay cả khi đó, nó sẽ không phải là một thuật toán chậm, nó sẽ là một triển khai chậm. Trong thực tế, hướng đối tượng sẽ cho phép bạn thử các kịch bản giả định khác nhau (hoặc xem lại thuật toán trong tương lai) và do đó cung cấp các cải tiến thuật toán cho nó, điều mà bạn không bao giờ có thể hy vọng đạt được nếu bạn đã viết nó theo cách spaghetti trước nơi, bởi vì nhiệm vụ sẽ nản chí. (Về cơ bản bạn sẽ phải viết lại toàn bộ.)

Ví dụ, bằng cách chia các nhiệm vụ và thực thể khác nhau cho các đối tượng cắt sạch, bạn có thể dễ dàng truy cập sau và, giả sử, nhúng một thiết bị lưu trữ giữa một số đối tượng, (trong suốt với chúng), có thể mang lại hàng ngàn cải thiện gấp.

Nói chung, các loại cải tiến bạn có thể đạt được bằng cách sử dụng ngôn ngữ cấp thấp (hoặc thủ thuật thông minh với ngôn ngữ cấp cao) mang lại sự cải tiến về thời gian (tuyến tính) không đổi, không tính theo ký hiệu lớn. Với các cải tiến thuật toán, bạn có thể đạt được các cải tiến phi tuyến tính. Đó là vô giá.


1
+1: sự khác biệt giữa spaghetti và mã hướng đối tượng (hoặc mã được viết theo mô hình được xác định rõ) là: mỗi phiên bản mã tốt được viết lại mang lại hiểu biết mới về vấn đề. Mỗi phiên bản của spaghetti viết lại không bao giờ mang lại bất kỳ cái nhìn sâu sắc.
rwong

@rwong không thể giải thích rõ hơn ;-)
umlcat

3

Nhưng OOP này có thể là một bất lợi cho phần mềm dựa trên hiệu suất, tức là chương trình thực thi nhanh như thế nào?

Thường thì có !!! NHƯNG...

Nói cách khác, nhiều tham chiếu giữa nhiều đối tượng khác nhau, hoặc sử dụng nhiều phương thức từ nhiều lớp, có thể dẫn đến việc thực hiện "nặng" không?

Không cần thiết. Điều này phụ thuộc vào ngôn ngữ / trình biên dịch. Ví dụ, trình biên dịch C ++ tối ưu hóa, với điều kiện bạn không sử dụng các hàm ảo, thường sẽ đè bẹp đối tượng của bạn xuống không. Bạn có thể thực hiện những việc như viết một trình bao bọc trên intđó hoặc một con trỏ thông minh có phạm vi trên một con trỏ cũ đơn giản, hoạt động nhanh như sử dụng trực tiếp các loại dữ liệu cũ đơn giản này.

Trong các ngôn ngữ khác như Java, có một chút chi phí cho một đối tượng (thường khá nhỏ trong nhiều trường hợp, nhưng thiên văn học trong một số trường hợp hiếm hoi có các đối tượng thực sự tuổi teen). Ví dụ, Integercó hiệu quả thấp hơn đáng kể so với int(lấy 16 byte so với 4 trên 64 bit). Tuy nhiên, đây không chỉ là chất thải trắng trợn hoặc bất cứ thứ gì thuộc loại đó. Đổi lại, Java cung cấp những thứ như sự phản chiếu trên mọi loại do người dùng định nghĩa thống nhất, cũng như khả năng ghi đè bất kỳ chức năng nào không được đánh dấu là final.

Tuy nhiên, hãy lấy kịch bản trường hợp tốt nhất: trình biên dịch C ++ tối ưu hóa có thể tối ưu hóa giao diện đối tượng xuống không chi phí. Thậm chí sau đó, OOP thường sẽ làm giảm hiệu suất và ngăn không cho nó đạt đến đỉnh cao. Điều đó có vẻ như là một nghịch lý hoàn toàn: làm thế nào nó có thể? Vấn đề nằm ở:

Thiết kế giao diện và đóng gói

Vấn đề là ngay cả khi trình biên dịch có thể nén cấu trúc của một đối tượng xuống không chi phí (điều này ít nhất là rất đúng để tối ưu hóa trình biên dịch C ++), việc đóng gói và thiết kế giao diện (và phụ thuộc tích lũy) của các đối tượng hạt mịn thường sẽ ngăn chặn hầu hết các biểu diễn dữ liệu tối ưu cho các đối tượng được dự định tổng hợp bởi số đông (thường là trường hợp đối với phần mềm quan trọng về hiệu năng).

Lấy ví dụ này:

class Particle
{
public:
    ...

private:
    double birth;                // 8 bytes
    float x;                     // 4 bytes
    float y;                     // 4 bytes
    float z;                     // 4 bytes
    /*padding*/                  // 4 bytes of padding
};
Particle particles[1000000];     // 1mil particles (~24 megs)

Giả sử mô hình truy cập bộ nhớ của chúng ta chỉ đơn giản là lặp qua các hạt này một cách tuần tự và di chuyển chúng xung quanh từng khung hình, đưa chúng ra khỏi các góc của màn hình và sau đó hiển thị kết quả.

Chúng ta có thể thấy một phần đệm 4 byte sáng chói cần thiết để sắp xếp birththành viên đúng cách khi các hạt được tổng hợp liên tục. Đã ~ 16,7% bộ nhớ bị lãng phí với không gian chết được sử dụng để căn chỉnh.

Điều này có vẻ không ổn vì chúng ta có hàng gigabyte DRAM những ngày này. Tuy nhiên, ngay cả những cỗ máy quái thú nhất chúng ta có ngày nay thường chỉ có 8 megabyte khi nói đến vùng chậm nhất và lớn nhất của bộ đệm CPU (L3). Chúng ta càng ít có thể phù hợp ở đó, chúng ta càng trả nhiều tiền hơn cho việc truy cập DRAM lặp đi lặp lại và những thứ chậm hơn nhận được. Đột nhiên, lãng phí 16,7% bộ nhớ dường như không còn là một thỏa thuận tầm thường.

Chúng tôi có thể dễ dàng loại bỏ chi phí này mà không có bất kỳ tác động nào đến việc căn chỉnh trường:

class Particle
{
public:
    ...

private:
    float x;                     // 4 bytes
    float y;                     // 4 bytes
    float z;                     // 4 bytes
};
Particle particles[1000000];     // 1mil particles (~12 megs)
double particle_birth[1000000];  // 1mil particle births (~8 bytes)

Bây giờ chúng tôi đã giảm bộ nhớ từ 24 megs xuống còn 20 megs. Với kiểu truy cập tuần tự, giờ đây máy sẽ tiêu thụ dữ liệu này nhanh hơn một chút.

Nhưng hãy nhìn vào birthlĩnh vực này kỹ hơn một chút. Giả sử nó ghi lại thời gian bắt đầu khi một hạt được sinh ra (được tạo ra). Hãy tưởng tượng trường chỉ được truy cập khi một hạt được tạo lần đầu tiên và cứ sau 10 giây để xem một hạt có chết đi và được tái sinh ở một vị trí ngẫu nhiên trên màn hình hay không. Trong trường hợp đó, birthlà một lĩnh vực lạnh. Nó không được truy cập trong các vòng lặp hiệu suất quan trọng của chúng tôi.

Kết quả là, dữ liệu quan trọng về hiệu năng thực tế không phải là 20 megabyte mà thực sự là một khối liền kề 12 megabyte. Bộ nhớ nóng thực tế chúng ta truy cập thường xuyên đã giảm xuống một nửa kích thước của nó! Yêu cầu tăng tốc đáng kể so với giải pháp 24 megabyte ban đầu của chúng tôi (không cần đo lường - đã thực hiện loại công cụ này hàng ngàn lần, nhưng hãy yên tâm nếu nghi ngờ).

Tuy nhiên, hãy chú ý những gì chúng tôi đã làm ở đây. Chúng tôi đã phá vỡ hoàn toàn việc đóng gói của đối tượng hạt này. Trạng thái của nó hiện được phân chia giữa Particlecác trường riêng của một loại và một mảng song song riêng biệt. Và đó là nơi thiết kế hướng đối tượng dạng hạt cản trở.

Chúng ta không thể biểu thị biểu diễn dữ liệu tối ưu khi giới hạn trong thiết kế giao diện của một đối tượng rất đơn giản như một hạt, một pixel, thậm chí là một vectơ 4 thành phần, thậm chí có thể là một đối tượng "sinh vật" duy nhất trong trò chơi , v.v. Tốc độ của một con báo sẽ bị lãng phí nếu nó đứng trên một hòn đảo tuổi teen rộng 2 mét vuông, và đó là điều mà thiết kế hướng đối tượng rất chi tiết thường làm về mặt hiệu suất. Nó giới hạn biểu diễn dữ liệu với bản chất phụ tối ưu.

Để giải quyết vấn đề này, giả sử rằng chúng ta chỉ di chuyển các hạt xung quanh, chúng ta thực sự có thể truy cập các trường x / y / z của chúng trong ba vòng riêng biệt. Trong trường hợp đó, chúng ta có thể hưởng lợi từ nội tại SIMD kiểu SoA với các thanh ghi AVX có thể vector hóa 8 hoạt động SPFP song song. Nhưng để làm điều này, bây giờ chúng ta phải sử dụng đại diện này:

float particle_x[1000000];       // 1mil particle X positions (~4 megs)
float particle_y[1000000];       // 1mil particle Y positions (~4 megs)
float particle_z[1000000];       // 1mil particle Z positions (~4 megs)
double particle_birth[1000000];  // 1mil particle births (~8 bytes)

Bây giờ chúng ta đang bay với mô phỏng hạt, nhưng hãy nhìn những gì đã xảy ra với thiết kế hạt của chúng ta. Nó đã bị phá hủy hoàn toàn, và chúng tôi hiện đang xem xét 4 mảng song song và không có đối tượng nào để tổng hợp chúng. ParticleThiết kế hướng đối tượng của chúng tôi đã đi sayonara.

Điều này đã xảy ra với tôi nhiều lần làm việc trong các lĩnh vực quan trọng về hiệu suất, nơi người dùng yêu cầu tốc độ chỉ với sự chính xác là điều họ yêu cầu nhiều hơn. Những thiết kế hướng đối tượng nhỏ bé này phải được phá hủy, và sự phá vỡ tầng thường yêu cầu chúng tôi sử dụng chiến lược khấu hao chậm đối với thiết kế nhanh hơn.

Giải pháp

Kịch bản trên chỉ đưa ra một vấn đề với các thiết kế hướng đối tượng chi tiết . Trong những trường hợp đó, chúng tôi thường phải phá hủy cấu trúc để thể hiện các biểu diễn hiệu quả hơn do các đại diện SoA, phân tách trường nóng / lạnh, giảm đệm cho các mẫu truy cập tuần tự (đệm đôi khi hữu ích cho hiệu suất với truy cập ngẫu nhiên các mẫu trong các trường hợp AoS, nhưng hầu như luôn là một trở ngại cho các mẫu truy cập tuần tự), v.v.

Tuy nhiên, chúng ta có thể lấy đại diện cuối cùng mà chúng ta đã giải quyết và vẫn mô hình hóa một giao diện hướng đối tượng:

// Represents a collection of particles.
class ParticleSystem
{
public:
    ...

private:
    double particle_birth[1000000];  // 1mil particle births (~8 bytes)
    float particle_x[1000000];       // 1mil particle X positions (~4 megs)
    float particle_y[1000000];       // 1mil particle Y positions (~4 megs)
    float particle_z[1000000];       // 1mil particle Z positions (~4 megs)
};

Bây giờ chúng tôi tốt. Chúng ta có thể nhận được tất cả các tính năng hướng đối tượng mà chúng ta thích. Cheetah có cả một đất nước để chạy qua nhanh nhất có thể. Thiết kế giao diện của chúng tôi không còn bẫy chúng tôi vào một góc cổ chai.

ParticleSystemthậm chí có thể trừu tượng và sử dụng các chức năng ảo. Bây giờ, chúng ta đang trả tiền cho chi phí chung ở cấp tập hợp các hạt thay vì ở cấp độ mỗi hạt . Chi phí hoạt động là 1 / 1.000.000 so với mức khác nếu chúng ta mô hình hóa các đối tượng ở cấp hạt riêng lẻ.

Vì vậy, đó là giải pháp trong các lĩnh vực quan trọng về hiệu năng thực sự xử lý tải nặng và cho tất cả các loại ngôn ngữ lập trình (kỹ thuật này mang lại lợi ích cho C, C ++, Python, Java, JavaScript, Lua, Swift, v.v.). Và nó không thể dễ dàng được gắn nhãn là "tối ưu hóa sớm", vì điều này liên quan đến thiết kế giao diệnkiến trúc . Chúng ta không thể viết một cơ sở mã hóa mô hình hóa một hạt như một đối tượng với một khối lượng phụ thuộc máy khách vào mộtParticle'sgiao diện công cộng và sau đó thay đổi tâm trí của chúng tôi sau này. Tôi đã làm điều đó rất nhiều khi được gọi để tối ưu hóa các cơ sở mã kế thừa và cuối cùng có thể mất hàng tháng để viết lại hàng chục ngàn dòng mã một cách cẩn thận để sử dụng thiết kế cồng kềnh. Điều này lý tưởng ảnh hưởng đến cách chúng tôi thiết kế mọi thứ trả trước với điều kiện là chúng tôi có thể dự đoán được một tải nặng.

Tôi tiếp tục lặp lại câu trả lời này dưới hình thức này hay hình thức khác trong nhiều câu hỏi về hiệu suất, và đặc biệt là những câu hỏi liên quan đến thiết kế hướng đối tượng. Thiết kế hướng đối tượng vẫn có thể tương thích với nhu cầu hiệu suất cao nhất, nhưng chúng ta phải thay đổi cách chúng ta nghĩ về nó một chút. Chúng ta phải cho con báo đó một số phòng để chạy nhanh nhất có thể, và điều đó thường là không thể nếu chúng ta thiết kế những vật thể nhỏ bé mà hầu như không lưu trữ bất kỳ trạng thái nào.


Tuyệt diệu. Đây là những gì tôi đã thực sự tìm kiếm về việc kết hợp OOP với nhu cầu hiệu suất cao. Tôi thực sự không thể hiểu tại sao nó không được nâng cấp nhiều hơn.
pbx

2

Đúng, tư duy hướng đối tượng chắc chắn có thể là trung lập hoặc tiêu cực khi nói đến lập trình hiệu suất cao, cả ở cấp độ thuật toán và triển khai. Nếu OOP thay thế phân tích thuật toán, nó có thể dẫn bạn đến việc triển khai sớm và ở mức thấp nhất, các tóm tắt OOP phải được đặt sang một bên.

Vấn đề bắt nguồn từ sự nhấn mạnh của OOP trong việc suy nghĩ về các trường hợp riêng lẻ. Tôi nghĩ thật công bằng khi nói rằng cách suy nghĩ của OOP về một thuật toán là bằng cách nghĩ về một bộ giá trị cụ thể và thực hiện theo cách đó. Nếu đó là con đường cấp cao nhất của bạn, bạn không có khả năng nhận ra một sự chuyển đổi hoặc tái cấu trúc sẽ dẫn đến lợi ích của Big O.

Ở cấp độ thuật toán, người ta thường nghĩ về bức tranh lớn hơn và các ràng buộc hoặc mối quan hệ giữa các giá trị dẫn đến lợi ích của Big O. Một ví dụ có thể là không có gì trong suy nghĩ OOP sẽ dẫn bạn chuyển đổi "tổng một phạm vi số nguyên liên tục" từ một vòng lặp sang(max + min) * n/2

Ở cấp độ triển khai, mặc dù các máy tính "đủ nhanh" cho hầu hết các thuật toán ở cấp ứng dụng, nhưng trong mã hiệu suất cấp thấp, người ta lo lắng rất nhiều về địa phương. Một lần nữa, OOP nhấn mạnh vào việc suy nghĩ về một cá thể riêng lẻ và các giá trị của một lần đi qua vòng lặp có thể là một tiêu cực. Trong mã hiệu suất cao, thay vì viết một vòng lặp đơn giản, bạn có thể muốn hủy một phần vòng lặp, nhóm một số hướng dẫn tải lên ở trên cùng, sau đó chuyển đổi chúng thành một nhóm, sau đó viết chúng thành một nhóm. Trong khi đó, bạn sẽ chú ý đến các tính toán trung gian và, hết sức, để truy cập bộ nhớ cache và bộ nhớ; các vấn đề trong đó trừu tượng OOP không còn hiệu lực. Và, nếu theo sau, có thể gây hiểu nhầm: ở cấp độ này, bạn phải biết và suy nghĩ về các đại diện ở cấp độ máy.

Khi bạn nhìn vào thứ gì đó giống như Hiệu suất nguyên thủy của Intel, bạn có hàng ngàn triển khai Biến đổi Fourier nhanh, mỗi cái được điều chỉnh để hoạt động tốt hơn cho kiến ​​trúc máy và kích thước dữ liệu cụ thể. (Thật hấp dẫn, hóa ra phần lớn các triển khai này được tạo bằng máy: Lập trình hiệu suất tự động Markus Püschel )

Tất nhiên, như hầu hết các câu trả lời đã nói, đối với hầu hết các phát triển, đối với hầu hết các thuật toán, OOP không liên quan đến hiệu suất. Miễn là bạn không "bi quan sớm" và thêm vào rất nhiều cuộc gọi không phải cục bộ, thiscon trỏ không ở đây cũng không ở đó.


0

Nó liên quan, và thường bị bỏ qua.

Đó không phải là một câu trả lời dễ dàng, nó phụ thuộc vào những gì bạn muốn làm.

Một số thuật toán có hiệu suất tốt hơn khi sử dụng lập trình có cấu trúc đơn giản, trong khi các thuật toán khác sử dụng hướng đối tượng tốt hơn.

Trước Định hướng đối tượng, nhiều trường dạy thiết kế thuật toán (ed) với lập trình có cấu trúc. Ngày nay, nhiều trường học, dạy lập trình hướng đối tượng, bỏ qua thiết kế và hiệu suất thuật toán ..

Tất nhiên, ở đó các trường dạy lập trình có cấu trúc, hoàn toàn không quan tâm đến thuật toán.


0

Hiệu suất tất cả đều đến với chu kỳ CPU và bộ nhớ cuối cùng. Nhưng sự khác biệt về tỷ lệ phần trăm giữa chi phí của tin nhắn và đóng gói OOP và ngữ nghĩa lập trình mở rộng hơn có thể hoặc không phải là một tỷ lệ đủ đáng kể để tạo ra sự khác biệt đáng chú ý trong hiệu suất ứng dụng của bạn. Nếu một ứng dụng là đĩa hoặc bộ nhớ cache dữ liệu bị ràng buộc, mọi chi phí OOP có thể bị mất hoàn toàn trong tiếng ồn.

Tuy nhiên, trong các vòng lặp bên trong của xử lý tín hiệu và hình ảnh thời gian thực và các ứng dụng ràng buộc tính toán số khác, sự khác biệt có thể là một tỷ lệ đáng kể của chu kỳ CPU và bộ nhớ, có thể khiến bất kỳ chi phí OOP nào tốn kém hơn khi thực hiện.

Các ngữ nghĩa của một ngôn ngữ OOP cụ thể có thể hoặc không thể tạo ra đủ cơ hội cho trình biên dịch để tối ưu hóa các chu kỳ đó hoặc để các mạch dự đoán nhánh của CPU luôn đoán chính xác và bao quát các chu trình đó bằng cách tìm nạp trước và đường ống.


0

Một thiết kế hướng đối tượng tốt đã giúp tôi tăng tốc một ứng dụng đáng kể. A đã phải tạo ra đồ họa phức tạp theo một cách thuật toán. Tôi đã làm điều đó thông qua tự động Microsoft Visio. Tôi đã làm việc, nhưng rất chậm. May mắn thay, tôi đã chèn thêm một mức độ trừu tượng giữa logic (thuật toán) và công cụ Visio. Thành phần Visio của tôi đã làm lộ chức năng của nó thông qua một giao diện. Điều này cho phép tôi dễ dàng thay thế thành phần chậm bằng một tệp SVG khác, nhanh hơn ít nhất 50 lần! Nếu không có cách tiếp cận hướng đối tượng rõ ràng, các mã cho thuật toán và điều khiển Tầm nhìn sẽ bị vướng vào một cách, điều này sẽ biến sự thay đổi thành một cơn ác mộng.


ý bạn là OO Design được áp dụng với ngôn ngữ thủ tục hay ngôn ngữ lập trình OO Design & OO?
umlcat

Tôi đang nói về một ứng dụng C #. Cả thiết kế và ngôn ngữ đều là OO Trong khi OO-iness của ngôn ngữ sẽ giới thiệu một số lần thực hiện nhỏ (gọi phương thức ảo, tạo đối tượng, truy cập thành viên qua giao diện), thiết kế OO giúp tôi tạo ra một ứng dụng nhanh hơn nhiều. Điều tôi muốn nói là: Quên các lượt truy cập hiệu suất do OO (ngôn ngữ và thiết kế). Trừ khi bạn đang thực hiện các phép tính nặng nề với hàng triệu lần lặp, OO sẽ không gây hại cho bạn. Nơi bạn thường mất nhiều thời gian là I / O.
Olivier Jacot-Descombes
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.