Tôi thích nghĩ về hiệu suất theo " giới hạn ". Đó là một cách thuận tiện để khái niệm một hệ thống kết nối khá phức tạp. Khi bạn gặp vấn đề về hiệu suất, bạn đặt câu hỏi: "Tôi đang đạt đến giới hạn nào?" (Hoặc: "Tôi có bị ràng buộc CPU / GPU không?")
Bạn có thể chia nó thành nhiều cấp độ. Ở cấp độ cao nhất, bạn có CPU và GPU. Bạn có thể bị ràng buộc CPU (GPU ngồi chờ CPU) hoặc ràng buộc GPU (CPU đang chờ GPU). Đây là một bài viết blog tốt về chủ đề này.
Bạn có thể phá vỡ nó hơn nữa. Về phía CPU , bạn có thể đang sử dụng tất cả các chu kỳ của mình trên dữ liệu đã có trong bộ đệm CPU. Hoặc bạn có thể bị giới hạn bộ nhớ , khiến CPU không hoạt động khi chờ dữ liệu đến từ bộ nhớ chính ( để tối ưu hóa bố cục dữ liệu của bạn ). Bạn có thể phá vỡ nó hơn nữa vẫn còn.
(Trong khi tôi đang thực hiện tổng quan rộng về hiệu suất liên quan đến XNA, tôi sẽ chỉ ra rằng việc phân bổ loại tham chiếu ( class
không struct
), trong khi giá rẻ, có thể kích hoạt trình thu gom rác, sẽ đốt cháy rất nhiều chu kỳ - đặc biệt là trên Xbox 360 . Xem ở đây để biết chi tiết).
Về phía GPU , tôi sẽ bắt đầu bằng cách chỉ cho bạn bài đăng blog tuyệt vời này có rất nhiều chi tiết. Nếu bạn muốn một mức độ chi tiết điên rồ trên đường ống, hãy đọc loạt bài viết trên blog này . ( Đây là một đơn giản hơn ).
Nói một cách đơn giản, một số trong những cái lớn là: " giới hạn lấp đầy " (bạn có thể ghi bao nhiêu pixel vào backbuffer - thường là bạn có thể rút bao nhiêu tiền), " giới hạn shader " (mức độ shader của bạn có thể phức tạp đến mức nào) bạn có thể đẩy bao nhiêu dữ liệu qua chúng), " giới hạn kết cấu tìm nạp / kết cấu băng thông " (bao nhiêu dữ liệu kết cấu bạn có thể truy cập).
Và bây giờ, chúng ta đến với vấn đề lớn - đó là những gì bạn thực sự yêu cầu - nơi CPU và GPU phải tương tác (thông qua các trình điều khiển và API khác nhau). Một cách lỏng lẻo là " giới hạn lô " và " băng thông ". (Lưu ý rằng phần một của loạt tôi đã đề cập trước đó đi vào sâu rộng thêm chi tiết.)
Nhưng, về cơ bản, một lô ( như bạn đã biết ) xảy ra bất cứ khi nào bạn gọi một trong các GraphicsDevice.Draw*
hàm (hoặc một phần của XNA, như SpriteBatch
, điều này sẽ giúp bạn). Vì bạn chắc chắn đã đọc, bạn nhận được vài nghìn * trong số này trên mỗi khung. Đây là giới hạn CPU - vì vậy nó cạnh tranh với việc sử dụng CPU khác của bạn. Về cơ bản, trình điều khiển đóng gói mọi thứ về những gì bạn đã bảo nó vẽ và gửi nó tới GPU.
Và sau đó là băng thông cho GPU. Đây là bao nhiêu dữ liệu thô bạn có thể chuyển ở đó. Điều này bao gồm tất cả các thông tin trạng thái đi kèm theo lô - mọi thứ từ cài đặt trạng thái kết xuất và các tham số / tham số đổ bóng (bao gồm những thứ như ma trận thế giới / khung nhìn / dự án), cho đến các đỉnh khi sử dụng các DrawUser*
hàm. Nó cũng bao gồm bất kỳ cuộc gọi đến SetData
và GetData
trên kết cấu, bộ đệm đỉnh, v.v.
Tại thời điểm này tôi nên nói rằng bất cứ điều gì bạn có thể gọi SetData
(kết cấu, bộ đệm đỉnh và chỉ mục, v.v.), cũng như Effect
s - vẫn còn trong bộ nhớ GPU. Nó không liên tục được gửi lại cho GPU. Lệnh draw tham chiếu dữ liệu đó được gửi đơn giản bằng một con trỏ tới dữ liệu đó.
(Ngoài ra: bạn chỉ có thể gửi lệnh vẽ từ luồng chính, nhưng bạn có thể SetData
trên bất kỳ luồng nào.)
XNA làm phức tạp mọi thứ hơi với khiến lớp trạng thái của nó ( BlendState
, DepthStencilState
, vv). Dữ liệu trạng thái này được gửi cho mỗi cuộc gọi bốc thăm (trong mỗi đợt). Tôi không chắc chắn 100%, nhưng tôi có ấn tượng rằng nó được gửi một cách lười biếng (nó chỉ gửi trạng thái thay đổi). Dù bằng cách nào, thay đổi trạng thái là rẻ đến mức miễn phí, liên quan đến chi phí của một đợt.
Cuối cùng, điều cuối cùng cần đề cập là đường ống GPU bên trong . Bạn không muốn buộc nó phải xóa bằng cách ghi vào dữ liệu mà nó vẫn cần đọc hoặc đọc dữ liệu mà nó vẫn cần ghi. Một đường ống xả có nghĩa là nó chờ các hoạt động kết thúc, để mọi thứ ở trạng thái nhất quán khi dữ liệu được truy cập.
Hai trường hợp cụ thể cần chú ý là: Gọi GetData
bất cứ thứ gì động - đặc biệt là RenderTarget2D
GPU có thể được ghi vào. Điều này cực kỳ tệ cho hiệu suất - đừng làm điều đó.
Trường hợp khác là gọi SetData
bộ đệm đỉnh / chỉ mục. Nếu bạn cần làm điều này thường xuyên, sử dụng một DynamicVertexBuffer
(cũng DynamicIndexBuffer
). Điều này cho phép GPU biết rằng chúng sẽ thay đổi thường xuyên và thực hiện một số phép thuật đệm trong nội bộ để tránh hiện tượng lộn đường ống.
(Cũng lưu ý rằng bộ đệm động nhanh hơn DrawUser*
các phương thức - nhưng chúng phải được phân bổ trước ở kích thước tối đa được yêu cầu.)
... Và đó là tất cả mọi thứ tôi biết về hiệu suất XNA :)