Có phải Java khó hơn nhiều so với tinh chỉnh tinh chỉnh cho hiệu năng so với C / C ++? [đóng cửa]


11

Liệu "phép thuật" của JVM có cản trở tầm ảnh hưởng của một lập trình viên đối với các tối ưu hóa vi mô trong Java không? Gần đây tôi đã đọc trong C ++, đôi khi thứ tự của các thành viên dữ liệu có thể cung cấp tối ưu hóa (được cấp, trong môi trường micro giây) và tôi cho rằng tay của một lập trình viên bị trói khi nói đến việc ép hiệu năng từ Java?

Tôi đánh giá cao một thuật toán tốt cung cấp tốc độ tăng tốc lớn hơn, nhưng một khi bạn có thuật toán chính xác thì Java khó điều chỉnh hơn do điều khiển JVM?

Nếu không, mọi người có thể đưa ra ví dụ về những thủ thuật bạn có thể sử dụng trong Java (bên cạnh các cờ trình biên dịch đơn giản).


14
Nguyên tắc cơ bản đằng sau tất cả tối ưu hóa Java là: JVM có thể đã thực hiện nó tốt hơn bạn có thể. Tối ưu hóa chủ yếu liên quan đến việc tuân theo các thực hành lập trình hợp lý và tránh những điều thông thường như nối các chuỗi trong một vòng lặp.
Robert Harvey

3
Nguyên tắc tối ưu hóa vi mô trong tất cả các ngôn ngữ là trình biên dịch đã thực hiện nó tốt hơn bạn có thể. Một nguyên tắc khác của tối ưu hóa vi mô trong tất cả các ngôn ngữ là việc ném nhiều phần cứng vào nó rẻ hơn so với tối ưu hóa thời gian của lập trình viên. Lập trình viên phải có xu hướng mở rộng các vấn đề (thuật toán dưới mức tối ưu), nhưng tối ưu hóa vi mô là một sự lãng phí thời gian. Đôi khi, tối ưu hóa vi mô có ý nghĩa đối với các hệ thống nhúng, nơi bạn không thể ném thêm phần cứng vào nó, nhưng Android sử dụng Java và việc triển khai nó khá kém, cho thấy hầu hết trong số chúng đã có đủ phần cứng.
Jan Hudec

1
cho "thủ đoạn hiệu suất Java", giá trị nghiên cứu là: Effective Java , Angelika Langer Liên kết - Hiệu suất Java và hiệu suất liên quan đến bài viết của Brian Goetz trong lý thuyết và thực hành JavaThreading nhẹ loạt liệt kê ở đây
một loại muôi

2
Hãy cực kỳ cẩn thận về các mẹo và thủ thuật - JVM, hệ điều hành và phần cứng tiếp tục - tốt nhất bạn nên học phương pháp điều chỉnh hiệu suất và áp dụng các cải tiến cho môi trường cụ thể của mình :-)
Martijn Verburg

Trong một số trường hợp, VM có thể thực hiện tối ưu hóa trong thời gian chạy không thực tế để thực hiện tại thời gian biên dịch. Sử dụng bộ nhớ được quản lý có thể cải thiện hiệu suất, mặc dù nó cũng sẽ thường có dung lượng bộ nhớ cao hơn. Bộ nhớ không sử dụng được giải phóng khi thuận tiện, thay vì càng sớm càng tốt.
Brian

Câu trả lời:


5

Chắc chắn, ở mức tối ưu hóa vi mô, JVM sẽ thực hiện một số điều mà bạn sẽ có ít quyền kiểm soát so với C và C ++.

Mặt khác, sự đa dạng của các hành vi trình biên dịch với C và C ++ đặc biệt sẽ có tác động tiêu cực lớn hơn nhiều đến khả năng tối ưu hóa vi mô của bạn theo bất kỳ cách di động mơ hồ nào (ngay cả trên các phiên bản trình biên dịch).

Nó phụ thuộc vào loại dự án bạn đang điều chỉnh, môi trường bạn đang nhắm mục tiêu và vv. Và cuối cùng, điều đó không thực sự quan trọng vì bạn nhận được một vài đơn đặt hàng có kết quả tốt hơn từ việc tối ưu hóa thiết kế chương trình / cấu trúc dữ liệu / thuật toán.


Nó có thể quan trọng rất nhiều khi bạn thấy ứng dụng của mình không mở rộng quy mô trên các lõi
James

@james - quan tâm đến công phu?
Telastyn


1
@James, chia tỷ lệ trên các lõi có rất ít liên quan đến ngôn ngữ thực hiện (ngoại trừ Python!), Và, nhiều hơn nữa để làm với kiến ​​trúc ứng dụng.
James Anderson

29

Tối ưu hóa vi mô hầu như không bao giờ có giá trị thời gian, và hầu hết tất cả những điều dễ dàng được thực hiện tự động bởi trình biên dịch và thời gian chạy.

Tuy nhiên, có một lĩnh vực tối ưu hóa quan trọng trong đó C ++ và Java khác nhau cơ bản và đó là truy cập bộ nhớ hàng loạt. C ++ có quản lý bộ nhớ thủ công, có nghĩa là bạn có thể tối ưu hóa bố cục dữ liệu và kiểu truy cập của ứng dụng để sử dụng toàn bộ bộ nhớ cache. Điều này khá khó, hơi cụ thể đối với phần cứng bạn đang chạy (vì vậy hiệu suất có thể biến mất trên các phần cứng khác nhau), nhưng nếu được thực hiện đúng, nó có thể dẫn đến hiệu suất hoàn toàn ngoạn mục. Tất nhiên bạn phải trả tiền cho nó với tiềm năng cho tất cả các loại lỗi khủng khiếp.

Với một ngôn ngữ được thu thập rác như Java, loại tối ưu hóa này không thể được thực hiện trong mã. Một số có thể được thực hiện bởi thời gian chạy (tự động hoặc thông qua cấu hình, xem bên dưới) và một số chỉ là không thể (giá bạn phải trả cho việc được bảo vệ khỏi các lỗi quản lý bộ nhớ).

Nếu không, mọi người có thể đưa ra ví dụ về những thủ thuật bạn có thể sử dụng trong Java (bên cạnh các cờ trình biên dịch đơn giản).

Các cờ biên dịch không liên quan trong Java vì trình biên dịch Java hầu như không tối ưu hóa; thời gian chạy nào.

Và thực sự thời gian chạy Java có vô số tham số có thể được điều chỉnh, đặc biệt là liên quan đến trình thu gom rác. Không có gì "đơn giản" về các tùy chọn đó - mặc định là tốt cho hầu hết các ứng dụng và để có hiệu suất tốt hơn đòi hỏi bạn phải hiểu chính xác những gì các tùy chọn làm và cách ứng dụng của bạn hoạt động.


1
+1: về cơ bản những gì tôi đã viết trong câu trả lời của tôi, có thể là công thức tốt hơn.
Klaim

1
+1: Điểm rất tốt, được giải thích một cách rất súc tích: "Điều này khá khó ... nhưng nếu được thực hiện đúng, nó có thể dẫn đến hiệu suất hoàn toàn ngoạn mục. Tất nhiên bạn trả tiền cho nó với tiềm năng cho tất cả các loại lỗi khủng khiếp . "
Giorgio

1
@MartinBa: Bạn phải trả nhiều hơn cho việc tối ưu hóa quản lý bộ nhớ. Nếu bạn không cố gắng tối ưu hóa quản lý bộ nhớ, quản lý bộ nhớ C ++ không khó lắm (tránh hoàn toàn thông qua STL hoặc làm cho nó tương đối dễ dàng khi sử dụng RAII). Tất nhiên, việc triển khai RAII trong C ++ cần nhiều dòng mã hơn là không làm gì trong Java (nghĩa là, vì Java xử lý nó cho bạn).
Brian

3
@Martin Ba: Về cơ bản là có. Con trỏ lơ lửng, tràn bộ đệm, con trỏ chưa được khởi tạo, lỗi về số học con trỏ, tất cả những thứ đơn giản là không tồn tại mà không có quản lý bộ nhớ thủ công. Và tối ưu hóa truy cập bộ nhớ khá nhiều đòi hỏi bạn phải thực hiện nhiều quản lý bộ nhớ thủ công.
Michael Borgwardt

1
Có một vài điều bạn có thể làm trong java. Một là gộp nhóm đối tượng, tối đa hóa khả năng địa phương bộ nhớ của các đối tượng (không giống như C ++ nơi nó có thể đảm bảo vị trí bộ nhớ).
RokL

5

[...] (Được cấp, trong môi trường micro giây) [...]

Micro-giây cộng lại nếu chúng ta lặp đi lặp lại hàng triệu đến hàng tỷ thứ. Một phiên tối ưu hóa vtune / vi cá nhân từ C ++ (không cải tiến thuật toán):

T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds

Tất cả mọi thứ ngoài "đa luồng", "SIMD" (viết tay để đánh bại trình biên dịch) và tối ưu hóa bản vá 4 hóa trị là tối ưu hóa bộ nhớ ở cấp độ vi mô. Ngoài ra, mã ban đầu bắt đầu từ thời gian ban đầu là 32 giây đã được tối ưu hóa khá nhiều (độ phức tạp thuật toán tối ưu về mặt lý thuyết) và đây là phiên gần đây. Phiên bản gốc dài trước khi phiên gần đây này mất hơn 5 phút để xử lý.

Tối ưu hóa hiệu quả bộ nhớ có thể giúp thường xuyên ở mọi nơi từ nhiều lần đến các mức độ lớn trong ngữ cảnh đơn luồng và hơn thế nữa trong bối cảnh đa luồng (lợi ích của một bộ nhớ hiệu quả thường nhân với nhiều luồng trong hỗn hợp).

Về tầm quan trọng của tối ưu hóa vi mô

Tôi có một chút kích động bởi ý tưởng này rằng tối ưu hóa vi mô là một sự lãng phí thời gian. Tôi đồng ý rằng đó là lời khuyên chung tốt, nhưng không phải ai cũng thực hiện sai dựa trên linh cảm và mê tín hơn là đo lường. Hoàn thành chính xác, nó không nhất thiết mang lại một tác động vi mô. Nếu chúng tôi lấy Embree (hạt nhân raytrac) của Intel và chỉ kiểm tra BVH vô hướng đơn giản mà họ đã viết (không phải gói tia khó đánh bại theo cấp số nhân), và sau đó cố gắng đánh bại hiệu năng của cấu trúc dữ liệu đó, thì đó có thể là hầu hết kinh nghiệm khiêm tốn ngay cả đối với một cựu chiến binh được sử dụng để định hình và điều chỉnh mã trong nhiều thập kỷ. Và đó là tất cả vì áp dụng tối ưu hóa vi mô. Giải pháp của họ có thể xử lý hơn một trăm triệu tia mỗi giây khi tôi thấy các chuyên gia công nghiệp làm việc trong lĩnh vực raytracing có thể '

Không có cách nào để thực hiện một cách đơn giản một BVH chỉ với trọng tâm thuật toán và có được hơn một trăm triệu giao điểm tia chính mỗi giây so với bất kỳ trình biên dịch tối ưu hóa nào (ngay cả ICC của Intel). Một người đơn giản thường không nhận được một triệu tia mỗi giây. Nó đòi hỏi các giải pháp chất lượng chuyên nghiệp để thường nhận được vài triệu tia mỗi giây. Phải tối ưu hóa vi mô ở cấp độ Intel để có được hơn một trăm triệu tia mỗi giây.

Thuật toán

Tôi nghĩ tối ưu hóa vi mô không quan trọng miễn là hiệu suất không quan trọng ở mức độ từ vài phút đến vài giây, ví dụ, hoặc vài giờ đến vài phút. Nếu chúng ta lấy một thuật toán khủng khiếp như sắp xếp bong bóng và sử dụng nó trên một đầu vào hàng loạt làm ví dụ, và sau đó so sánh nó với việc triển khai cơ bản của sắp xếp hợp nhất, thì kết quả trước có thể mất vài tháng để xử lý, sau đó có thể là 12 phút của độ phức tạp bậc hai so với tuyến tính.

Sự khác biệt giữa tháng và phút có lẽ sẽ khiến hầu hết mọi người, ngay cả những người không làm việc trong các lĩnh vực quan trọng về hiệu suất, coi thời gian thực hiện là không thể chấp nhận nếu nó yêu cầu người dùng chờ đợi hàng tháng để có kết quả.

Trong khi đó, nếu chúng ta so sánh sắp xếp hợp nhất không tối ưu hóa vi mô với quicksort (không hoàn toàn vượt trội về mặt thuật toán so với sắp xếp hợp nhất và chỉ cung cấp các cải tiến cấp vi mô cho địa phương tham chiếu), thì quicksort được tối ưu hóa vi mô có thể kết thúc 15 giây trái ngược với 12 phút. Làm cho người dùng chờ 12 phút có thể được chấp nhận hoàn toàn (loại thời gian nghỉ giải lao).

Tôi nghĩ rằng sự khác biệt này có lẽ không đáng kể đối với hầu hết mọi người trong khoảng thời gian từ 12 phút đến 15 giây và đó là lý do tại sao tối ưu hóa vi mô thường được coi là vô dụng vì nó thường chỉ giống như sự khác biệt giữa phút và giây chứ không phải phút và tháng. Một lý do khác tôi nghĩ rằng nó được coi là vô dụng là nó thường được áp dụng cho các khu vực không quan trọng: một số khu vực nhỏ thậm chí không phải là khập khiễng và quan trọng mang lại sự khác biệt đáng ngờ 1% (rất có thể chỉ là tiếng ồn). Nhưng đối với những người quan tâm đến các loại chênh lệch thời gian này và sẵn sàng đo lường và thực hiện đúng, tôi nghĩ rằng đáng chú ý đến ít nhất là các khái niệm cơ bản về phân cấp bộ nhớ (cụ thể là các cấp cao hơn liên quan đến lỗi trang và lỗi bộ nhớ cache) .

Java để lại rất nhiều chỗ cho tối ưu hóa vi mô tốt

Phew, xin lỗi - với những lời tán tỉnh đó qua một bên:

Liệu "phép thuật" của JVM có cản trở tầm ảnh hưởng của một lập trình viên đối với các tối ưu hóa vi mô trong Java không?

Một chút nhưng không nhiều như mọi người có thể nghĩ nếu bạn làm đúng. Ví dụ: nếu bạn đang xử lý hình ảnh, bằng mã gốc với SIMD viết tay, đa luồng và tối ưu hóa bộ nhớ (các mẫu truy cập và thậm chí có thể biểu diễn tùy thuộc vào thuật toán xử lý hình ảnh), bạn có thể dễ dàng nghiền nát hàng trăm triệu pixel mỗi giây trong 32- pixel RGBA (kênh màu 8 bit) và đôi khi thậm chí là hàng tỷ mỗi giây.

Nếu bạn nói, không thể đến bất kỳ nơi nào gần với Java, tạo ra một Pixelđối tượng (chỉ riêng điều này sẽ làm tăng kích thước của pixel từ 4 byte lên 16 trên 64 bit).

Nhưng bạn có thể có thể tiến gần hơn rất nhiều nếu bạn tránh Pixelđối tượng, sử dụng một mảng byte và mô hình hóa một Imageđối tượng. Java vẫn khá thành thạo ở đó nếu bạn bắt đầu sử dụng các mảng dữ liệu cũ đơn giản. Tôi đã từng thử những thứ này trước đây trong Java và khá ấn tượng với điều kiện là bạn không tạo ra một loạt các vật thể nhỏ bé ở khắp mọi nơi lớn hơn 4 lần so với bình thường (ví dụ: sử dụng intthay vì Integer) và bắt đầu mô hình hóa các giao diện hàng loạt như một Imagegiao diện, không Pixelgiao diện. Tôi thậm chí còn mạo hiểm nói rằng Java có thể cạnh tranh với hiệu suất C ++ nếu bạn đang lặp qua dữ liệu cũ đơn thuần và không phải là các đối tượng (mảng lớn float, ví dụ, không Float).

Có lẽ điều quan trọng hơn cả kích thước bộ nhớ là một mảng intđảm bảo biểu diễn liền kề. Một mảng Integerkhông. Sự liên tục thường rất cần thiết cho địa phương tham chiếu vì điều đó có nghĩa là nhiều yếu tố (ví dụ: 16 ints) đều có thể phù hợp với một dòng bộ đệm duy nhất và có khả năng được truy cập cùng nhau trước khi trục xuất với các mẫu truy cập bộ nhớ hiệu quả. Trong khi đó, một đơn vị Integercó thể bị mắc kẹt ở đâu đó trong bộ nhớ với bộ nhớ xung quanh là không liên quan, chỉ để vùng bộ nhớ đó được tải vào một dòng bộ đệm chỉ để sử dụng một số nguyên duy nhất trước khi bị trục xuất so với 16 số nguyên. Ngay cả khi chúng ta có được may mắn và xung quanhIntegersTất cả đều nằm cạnh nhau trong bộ nhớ, chúng ta chỉ có thể ghép 4 dòng vào bộ đệm có thể truy cập trước khi bị trục xuất do kết quả Integerlớn hơn 4 lần và đó là trường hợp tốt nhất.

Và có rất nhiều tối ưu hóa vi mô đã có ở đó vì chúng ta hợp nhất theo cùng một cấu trúc / cấu trúc bộ nhớ. Các mẫu truy cập bộ nhớ có vấn đề cho dù bạn sử dụng ngôn ngữ nào, các khái niệm như ốp lát / chặn vòng lặp thường có thể được áp dụng thường xuyên hơn trong C hoặc C ++, nhưng chúng cũng có lợi cho Java.

Gần đây tôi đã đọc trong C ++, đôi khi thứ tự của các thành viên dữ liệu có thể cung cấp tối ưu hóa [...]

Thứ tự của các thành viên dữ liệu thường không quan trọng trong Java, nhưng đó chủ yếu là một điều tốt. Trong C và C ++, việc giữ trật tự của các thành viên dữ liệu thường rất quan trọng vì lý do ABI để trình biên dịch không gây rối với điều đó. Các nhà phát triển con người làm việc ở đó phải cẩn thận để làm những việc như sắp xếp các thành viên dữ liệu của họ theo thứ tự giảm dần (lớn nhất đến nhỏ nhất) để tránh lãng phí bộ nhớ vào phần đệm. Với Java, rõ ràng JIT có thể sắp xếp lại các thành viên cho bạn một cách nhanh chóng để đảm bảo căn chỉnh phù hợp trong khi giảm thiểu phần đệm, do đó, trường hợp đó, nó tự động hóa một cái gì đó mà các lập trình viên C và C ++ trung bình thường có thể làm kém và kết thúc lãng phí bộ nhớ theo cách đó ( điều này không chỉ lãng phí bộ nhớ, mà thường lãng phí tốc độ bằng cách tăng bước tiến giữa các cấu trúc AoS một cách không cần thiết và gây ra nhiều lỗi nhớ cache hơn). Nó ' Một điều rất robot để sắp xếp lại các lĩnh vực để giảm thiểu việc đệm, vì vậy lý tưởng là con người không đối phó với điều đó. Thời gian duy nhất mà sự sắp xếp trường có thể quan trọng theo cách đòi hỏi con người phải biết cách sắp xếp tối ưu là nếu đối tượng lớn hơn 64 byte và chúng ta sắp xếp các trường dựa trên mẫu truy cập (không phải là phần đệm tối ưu) - trong trường hợp đó có thể là một nỗ lực của con người nhiều hơn (đòi hỏi phải hiểu các đường dẫn quan trọng, một số trong đó là thông tin mà trình biên dịch không thể lường trước mà không biết người dùng sẽ làm gì với phần mềm).

Nếu không, mọi người có thể đưa ra ví dụ về những thủ thuật bạn có thể sử dụng trong Java (bên cạnh các cờ trình biên dịch đơn giản).

Sự khác biệt lớn nhất đối với tôi về mặt tâm lý tối ưu hóa giữa Java và C ++ là C ++ có thể cho phép bạn sử dụng các đối tượng nhiều hơn một chút (tuổi teen) so với Java trong kịch bản quan trọng về hiệu năng. Ví dụ, C ++ có thể bọc một số nguyên cho một lớp mà không có chi phí nào (điểm chuẩn ở mọi nơi). Java phải có chi phí đệm con trỏ theo kiểu con trỏ siêu dữ liệu trên mỗi đối tượng, đó là lý do tại sao Booleanlớn hơn boolean(nhưng đổi lại mang lại lợi ích thống nhất cho sự phản chiếu và khả năng ghi đè bất kỳ chức năng nào không được đánh dấu như finalđối với mỗi UDT đơn lẻ).

C ++ dễ dàng hơn một chút trong việc kiểm soát sự liên tục của bố cục bộ nhớ trên các trường không đồng nhất (ví dụ: xen kẽ các float và int vào một mảng thông qua một cấu trúc / lớp), vì địa phương không gian thường bị mất (hoặc ít nhất là mất kiểm soát) trong Java khi phân bổ các đối tượng thông qua GC.

... nhưng thường thì các giải pháp hiệu suất cao nhất sẽ thường phân tách chúng ra và sử dụng mẫu truy cập SoA trên các mảng dữ liệu cũ liền kề. Vì vậy, đối với các khu vực cần hiệu năng cao nhất, các chiến lược để tối ưu hóa bố cục bộ nhớ giữa Java và C ++ thường giống nhau và thường sẽ khiến bạn phá hủy các giao diện hướng đối tượng tuổi teen đó theo hướng giao diện kiểu bộ sưu tập có thể làm những việc như nóng / Tách trường lạnh, đại diện SoA, v.v ... Các đại diện AoSoA không đồng nhất dường như là không thể trong Java (trừ khi bạn chỉ sử dụng một mảng byte hoặc một cái gì đó tương tự), nhưng đó là những trường hợp hiếm hoi trong đó cả haicác mẫu truy cập ngẫu nhiên và ngẫu nhiên cần phải nhanh trong khi đồng thời có hỗn hợp các loại trường cho các trường nóng. Đối với tôi phần lớn sự khác biệt trong chiến lược tối ưu hóa (ở mức độ chung) giữa hai điều này là điều cần thiết nếu bạn đang đạt được hiệu suất cao nhất.

Sự khác biệt khác nhau nhiều hơn một chút nếu bạn chỉ đơn giản đạt được hiệu suất "tốt" - không thể làm được nhiều như vậy với các đối tượng nhỏ như Integerso với intPITA, đặc biệt là cách nó tương tác với thuốc generic . Đó là một chút khó khăn hơn để chỉ xây dựng một cấu trúc dữ liệu chung như một mục tiêu tối ưu hóa trung tâm trong Java mà các công trình cho int, floatvv trong khi tránh những UDT lớn hơn và đắt tiền, nhưng thường là lĩnh vực hoạt động quan trọng nhất sẽ đòi hỏi tay lăn cấu trúc dữ liệu của riêng bạn điều chỉnh cho một mục đích rất cụ thể dù sao đi nữa, nó chỉ gây khó chịu cho mã đang phấn đấu cho hiệu năng tốt nhưng không phải là hiệu suất cao nhất.

Đối tượng trên cao

Lưu ý rằng chi phí đối tượng Java (siêu dữ liệu và mất cục bộ không gian và mất tạm thời cục bộ sau chu kỳ GC ban đầu) thường rất lớn đối với những thứ thực sự nhỏ (như intso với Integer) đang được hàng triệu người lưu trữ trong một số cấu trúc dữ liệu phần lớn tiếp giáp và truy cập trong các vòng rất chặt chẽ. Dường như có rất nhiều sự nhạy cảm về chủ đề này, vì vậy tôi nên làm rõ rằng bạn không muốn lo lắng về chi phí đối tượng cho các đối tượng lớn như hình ảnh, chỉ là các đối tượng thực sự rất nhỏ như một pixel.

Nếu bất cứ ai cảm thấy nghi ngờ về phần này, tôi khuyên bạn nên tạo một điểm chuẩn giữa tổng một triệu ngẫu nhiên intsso với một triệu ngẫu nhiên Integersvà thực hiện điều này nhiều lần ( Integerssẽ cải tổ lại trong bộ nhớ sau một chu kỳ GC ban đầu).

Trick cuối cùng: Thiết kế giao diện rời khỏi phòng để tối ưu hóa

Vì vậy, mẹo Java cuối cùng như tôi thấy nếu bạn đang xử lý một nơi xử lý tải nặng trên các vật thể nhỏ (ví dụ: a Pixel, 4 vector, ma trận 4 x 4 Particle, thậm chí có thể là Accountnếu nó chỉ có một vài nhỏ các trường) là để tránh sử dụng các đối tượng cho những thứ thiếu niên này và sử dụng các mảng (có thể được kết nối với nhau) của dữ liệu cũ đơn giản. Các đối tượng sau đó trở thành giao diện bộ sưu tập như Image, ParticleSystem, Accounts, một tập hợp các ma trận hoặc vector vv những cá nhân có thể được truy cập bởi chỉ số, ví dụ: Đây cũng là một trong những thủ thuật thiết kế cuối cùng trong C và C ++, vì ngay cả khi không có overhead đối tượng cơ bản và bộ nhớ rời rạc, mô hình hóa giao diện ở cấp độ của một hạt duy nhất ngăn chặn các giải pháp hiệu quả nhất.


1
Xem xét rằng hiệu suất kém trong phần lớn có thể thực sự có cơ hội vượt trội về hiệu suất cao nhất trong các lĩnh vực quan trọng, tôi không nghĩ người ta hoàn toàn có thể coi thường lợi thế của việc có hiệu suất tốt một cách dễ dàng. Và thủ thuật biến một mảng các cấu trúc thành một cấu trúc của các mảng bị phá vỡ phần nào khi tất cả (hoặc gần như tất cả) các giá trị bao gồm một trong các cấu trúc ban đầu sẽ được truy cập cùng một lúc. BTW: Tôi thấy bạn đang khai quật rất nhiều bài viết cũ và thêm câu trả lời hay của riêng bạn, đôi khi là câu trả lời hay ;-)
Ded repeatator

1
@Ded repeatator Hy vọng tôi không làm phiền mọi người bằng cách va chạm quá nhiều! Điều này có một chút giận dữ một chút tuổi teen - có lẽ tôi nên cải thiện nó một chút. SoA so với AoS thường là một khó khăn đối với tôi (truy cập tuần tự so với ngẫu nhiên). Tôi hiếm khi biết trả trước cái nào tôi nên sử dụng vì thường có sự kết hợp giữa truy cập ngẫu nhiên và ngẫu nhiên trong trường hợp của tôi. Bài học quý giá tôi thường học là thiết kế các giao diện không đủ chỗ để chơi với biểu diễn dữ liệu - giao diện cồng kềnh có thuật toán biến đổi lớn khi có thể (đôi khi không thể thực hiện được với các bit tuổi teen được truy cập ngẫu nhiên ở đây và ở đó).

1
Vâng, tôi chỉ nhận thấy bởi vì mọi thứ thực sự chậm. Và tôi đã dành thời gian của mình với từng người.
Ded repeatator

Tôi thực sự tự hỏi tại sao user204677đi xa. Thật là một câu trả lời tuyệt vời.
oligofren

3

Mặt khác, có một khu vực trung gian giữa tối ưu hóa vi mô và mặt khác là sự lựa chọn tốt về thuật toán.

Đây là khu vực của các yếu tố tăng tốc không đổi, và nó có thể mang lại các đơn đặt hàng lớn.
Cách thức thực hiện là bằng cách loại bỏ toàn bộ các phân số của thời gian thực hiện, như 30% đầu tiên, sau đó 20% những gì còn lại, sau đó là 50%, và cứ như vậy trong vài lần lặp lại, cho đến khi hầu như không còn gì.

Bạn không thấy điều này trong các chương trình kiểu demo nhỏ. Nơi bạn thấy nó nằm trong các chương trình nghiêm trọng lớn với nhiều cấu trúc dữ liệu lớp, trong đó ngăn xếp cuộc gọi thường sâu nhiều lớp. Một cách tốt để tìm các cơ hội tăng tốc là kiểm tra các mẫu thời gian ngẫu nhiên về trạng thái của chương trình.

Nói chung, việc tăng tốc bao gồm những thứ như:

  • giảm thiểu các cuộc gọi đến newbằng cách gộp và sử dụng lại các đối tượng cũ,

  • nhận ra những điều đang được thực hiện là vì mục đích chung, thay vì thực sự cần thiết,

  • sửa đổi cấu trúc dữ liệu bằng cách sử dụng các lớp bộ sưu tập khác nhau có cùng hành vi big-O nhưng tận dụng các mẫu truy cập thực sự được sử dụng,

  • lưu dữ liệu thu được bằng các lệnh gọi hàm thay vì gọi lại hàm, (Đây là xu hướng tự nhiên và gây cười của các lập trình viên khi cho rằng các hàm có tên ngắn hơn thực thi nhanh hơn.)

  • chịu đựng một số lượng không nhất quán giữa các cấu trúc dữ liệu dư thừa, trái ngược với việc cố gắng giữ chúng hoàn toàn phù hợp với các sự kiện thông báo,

  • Vân vân.

Nhưng tất nhiên không ai trong số những điều này nên được thực hiện mà không được chứng minh là có vấn đề bằng cách lấy mẫu.


2

Java (theo như tôi biết) cho phép bạn không kiểm soát các vị trí biến trong bộ nhớ để bạn khó tránh khỏi những thứ như chia sẻ sai và căn chỉnh các biến (bạn có thể loại bỏ một lớp với một số thành viên không sử dụng). Một điều nữa tôi không nghĩ bạn có thể tận dụng là các hướng dẫn như mmpause, nhưng những điều này là dành riêng cho CPU và vì vậy nếu bạn cho rằng bạn cần nó thì Java có thể không phải là ngôn ngữ để sử dụng.

Tồn tại lớp Không an toàn mang đến cho bạn sự linh hoạt của C / C ++ nhưng cũng có sự nguy hiểm của C / C ++.

Nó có thể giúp bạn xem mã lắp ráp mà JVM tạo cho mã của bạn

Để đọc về một ứng dụng Java nhìn vào loại chi tiết này, hãy xem mã Disruptor do LMAX phát hành


2

Câu hỏi này rất khó trả lời, bởi vì nó phụ thuộc vào việc thực hiện ngôn ngữ.

Nhìn chung, có rất ít chỗ cho những "tối ưu hóa vi mô" như vậy ngày nay. Lý do chính là trình biên dịch tận dụng tối ưu hóa như vậy trong quá trình biên dịch. Ví dụ, không có sự khác biệt về hiệu năng giữa các toán tử tăng trước và tăng sau trong các tình huống trong đó ngữ nghĩa của chúng giống hệt nhau. Một ví dụ khác là ví dụ một vòng lặp như thế này for(int i=0; i<vec.size(); i++)nơi người ta có thể lập luận rằng thay vì gọisize()hàm thành viên trong mỗi lần lặp sẽ tốt hơn nếu lấy kích thước của vectơ trước vòng lặp và sau đó so sánh với biến đơn đó và do đó tránh chức năng gọi một lần lặp. Tuy nhiên, có những trường hợp trình biên dịch sẽ phát hiện trường hợp ngớ ngẩn này và lưu lại kết quả. Tuy nhiên, điều này chỉ có thể khi hàm không có tác dụng phụ và trình biên dịch có thể chắc chắn rằng kích thước vectơ không đổi trong vòng lặp nên nó chỉ áp dụng cho các trường hợp khá nhỏ.


Đối với trường hợp thứ hai, tôi không nghĩ trình biên dịch có thể tối ưu hóa nó trong tương lai gần. Việc phát hiện ra rằng an toàn để tối ưu hóa vec.size () phụ thuộc vào việc chứng minh rằng kích thước nếu vectơ / mất không thay đổi bên trong vòng lặp, điều mà tôi tin là không thể giải quyết được do vấn đề tạm dừng.
Lie Ryan

@LieRyan Tôi đã thấy nhiều trường hợp (đơn giản) trong đó trình biên dịch đã tạo tệp nhị phân giống hệt nhau nếu kết quả được "lưu vào bộ nhớ cache" thủ công và nếu kích thước () được gọi. Tôi đã viết một số mã và hóa ra hành vi phụ thuộc rất nhiều vào cách thức hoạt động của chương trình. Có những trường hợp trình biên dịch có thể đảm bảo rằng không có khả năng kích thước vectơ thay đổi trong vòng lặp, và sau đó có những trường hợp không thể đảm bảo nó, rất giống với vấn đề tạm dừng như bạn đã đề cập. Hiện tại tôi không thể xác minh khiếu nại của mình (việc tháo gỡ C ++ là một nỗi đau) vì vậy tôi đã chỉnh sửa câu trả lời
zxcdw

2
@Lie Ryan: rất nhiều điều không thể giải quyết được trong trường hợp chung là hoàn toàn có thể quyết định cho các trường hợp cụ thể nhưng phổ biến, và đó thực sự là tất cả những gì bạn cần ở đây.
Michael Borgwardt

@LieRyan Nếu bạn chỉ gọi constcác phương thức trên vectơ này, tôi khá chắc chắn nhiều trình biên dịch tối ưu hóa sẽ tìm ra nó.
K.Steff

trong C # và tôi nghĩ tôi cũng đọc bằng Java, nếu bạn không kích thước bộ đệm, trình biên dịch biết rằng nó có thể loại bỏ các kiểm tra để xem nếu bạn đi ra ngoài giới hạn mảng và nếu bạn thực hiện kích thước bộ đệm thì phải kiểm tra , mà thường chi phí nhiều hơn bạn đang tiết kiệm bằng cách lưu trữ. Cố gắng để tối ưu hóa outsmart hiếm khi là một kế hoạch tốt.
Kate Gregory

1

mọi người có thể đưa ra ví dụ về những thủ thuật bạn có thể sử dụng trong Java (bên cạnh các cờ trình biên dịch đơn giản).

Khác với những cải tiến của thuật toán, hãy chắc chắn xem xét phân cấp bộ nhớ và cách bộ xử lý sử dụng nó. Có những lợi ích lớn trong việc giảm độ trễ truy cập bộ nhớ, khi bạn hiểu cách ngôn ngữ trong câu hỏi phân bổ bộ nhớ cho các loại dữ liệu và đối tượng của nó.

Ví dụ Java để truy cập vào một mảng 1000x1000 ints

Hãy xem xét mã mẫu dưới đây - nó truy cập vào cùng một vùng bộ nhớ (một mảng 1000x1000 của ints), nhưng theo một thứ tự khác. Trên mac mini của tôi (Core i7, 2,7 GHz), đầu ra như sau, cho thấy rằng việc truyền tải mảng theo hàng nhiều hơn gấp đôi hiệu suất (trung bình hơn 100 vòng mỗi lần).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

Điều này là do mảng được lưu trữ sao cho các cột liên tiếp (tức là giá trị int) được đặt liền kề trong bộ nhớ, trong khi các hàng liên tiếp thì không. Để bộ xử lý thực sự sử dụng dữ liệu, nó phải được chuyển đến bộ nhớ cache của nó. Việc truyền bộ nhớ bằng một khối byte, được gọi là dòng bộ đệm - tải một dòng bộ đệm trực tiếp từ bộ nhớ giới thiệu độ trễ và do đó làm giảm hiệu suất của chương trình.

Đối với Core i7 (cầu cát), một dòng bộ đệm chứa 64 byte, do đó mỗi lần truy cập bộ nhớ sẽ lấy 64 byte. Vì thử nghiệm đầu tiên truy cập bộ nhớ theo trình tự dự đoán, bộ xử lý sẽ tìm nạp trước dữ liệu trước khi chương trình thực sự được sử dụng. Nhìn chung, điều này dẫn đến độ trễ ít hơn khi truy cập bộ nhớ và do đó cải thiện hiệu suất.

Mã mẫu:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }

1

JVM có thể và thường xuyên can thiệp và trình biên dịch JIT có thể thay đổi đáng kể giữa các phiên bản Một số tối ưu hóa vi mô là không thể trong Java do giới hạn ngôn ngữ, chẳng hạn như thân thiện siêu phân luồng hoặc bộ sưu tập SIMD mới nhất của bộ xử lý Intel.

Một blog có nhiều thông tin về chủ đề từ một tác giả của Disruptor được khuyến nghị đọc:

Người ta luôn phải hỏi tại sao phải sử dụng Java nếu bạn muốn tối ưu hóa vi mô, có nhiều phương pháp thay thế để tăng tốc của một chức năng như sử dụng JNA hoặc JNI để truyền vào thư viện riêng.

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.