[...] (Đượ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 int
thay vì Integer
) và bắt đầu mô hình hóa các giao diện hàng loạt như một Image
giao diện, không Pixel
giao 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 Integer
khô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ị Integer
có 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 quanhIntegers
Tấ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ả Integer
lớ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 Boolean
lớ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ư Integer
so với int
PITA, đặ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
, float
vv 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ư int
so 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 ints
so với một triệu ngẫu nhiên Integers
và thực hiện điều này nhiều lần ( Integers
sẽ 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à Account
nế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.