
Mẹo tối ưu hiệu suất cho nhà phát triển game
Trang web này đã được dịch bằng máy để thuận tiện cho bạn. Chúng tôi không thể đảm bảo tính chính xác hoặc độ tin cậy của nội dung được dịch. Nếu bạn có thắc mắc về tính chính xác của nội dung được dịch, vui lòng tham khảo phiên bản tiếng Anh chính thức của trang web.
Hiệu suất mượt mà là điều cần thiết để tạo ra những trải nghiệm chơi game sống động cho người chơi. Để đảm bảo trò chơi của bạn được tối ưu hóa, một quy trình phân tích hiệu năng nhất quán từ đầu đến cuối là "điều bắt buộc" để phát triển trò chơi hiệu quả, và nó bắt đầu bằng một quy trình ba bước đơn giản:
- Hồ sơ trước khi thực hiện các thay đổi lớn: Thiết lập một đường cơ sở.
- Phân tích hiệu năng trong quá trình phát triển: Theo dõi và đảm bảo các thay đổi không làm ảnh hưởng đến hiệu suất hoặc ngân sách.
- Hồ sơ sau: Chứng minh những thay đổi đã có tác dụng như mong muốn.
Trang này phác thảo quy trình phân tích hồ sơ chung cho các nhà phát triển trò chơi. Trích từ sách điện tử, Hướng dẫn tối thượng để phân tích hiệu năng trò chơi Unity, có sẵn để tải xuống miễn phí (phiên bản Unity 6 của hướng dẫn sẽ sớm có). Cuốn sách điện tử được tạo ra bởi cả các chuyên gia Unity bên ngoài và bên trong về phát triển trò chơi, phân tích hiệu năng và tối ưu hóa.
Trong bài viết này bạn có thể tìm hiểu về các mục tiêu hữu ích cần đặt ra với việc lập hồ sơ (profiling), các điểm nghẽn hiệu suất phổ biến, chẳng hạn như bị giới hạn bởi CPU hoặc GPU, và cách xác định và điều tra các tình huống này chi tiết hơn.
Đặt ngân sách khung
Game thủ thường đo hiệu suất bằng tốc độ khung hình, hay khung hình trên giây (FPS), nhưng với tư cách là nhà phát triển, người ta thường khuyên nên sử dụng thời gian khung hình tính bằng mili giây thay thế. Hãy xem xét kịch bản đơn giản sau:
Trong quá trình chạy, trò chơi của bạn hiển thị 59 khung hình trong 0,75 giây. Tuy nhiên, khung hình tiếp theo mất 0,25 giây để kết xuất. Tốc độ khung hình trung bình được cung cấp là 60 FPS nghe có vẻ tốt, nhưng trên thực tế người chơi sẽ nhận thấy hiệu ứng giật vì khung hình cuối cùng mất một phần tư giây để kết xuất.
Đây là một trong những lý do tại sao việc đặt ra ngân sách thời gian cụ thể cho mỗi khung hình lại quan trọng. Điều này cung cấp cho bạn một mục tiêu vững chắc để hướng tới khi phân tích và tối ưu hóa trò chơi của mình, và cuối cùng, nó tạo ra trải nghiệm mượt mà và nhất quán hơn cho người chơi của bạn.
Mỗi khung hình sẽ có một ngân sách thời gian dựa trên FPS mục tiêu của bạn. Một ứng dụng nhắm mục tiêu 30 FPS luôn phải mất dưới 33,33 ms cho mỗi khung hình (1000 ms / 30 FPS). Tương tự, mục tiêu 60 FPS để lại 16,66 ms cho mỗi khung hình (1000 ms / 60 FPS).
Bạn có thể vượt quá ngân sách này trong các Sequences không tương tác, ví dụ, khi hiển thị menu giao diện người dùng hoặc tải cảnh, nhưng không phải trong quá trình chơi. Ngay cả một khung hình vượt quá ngân sách khung hình mục tiêu cũng sẽ gây giật.
Lưu ý: Tốc độ khung hình cao và ổn định trong các trò chơi VR là điều cần thiết để tránh gây buồn nôn hoặc khó chịu cho người chơi, và thường là cần thiết để trò chơi của bạn được nhà cung cấp nền tảng cấp chứng nhận.
Khung hình mỗi giây: Một chỉ số lừa dối
Một cách phổ biến mà game thủ đo lường hiệu suất là bằng tốc độ khung hình, hay khung hình trên giây. Tuy nhiên, bạn nên sử dụng thời gian khung hình tính bằng mili giây. Để hiểu tại sao, hãy xem biểu đồ trên về FPS so với thời gian khung hình.
Hãy xem xét những con số này:
1000 ms/giây / 900 FPS = 1.111 ms mỗi khung hình
1000 ms/giây / 450 FPS = 2.222 ms mỗi khung hình
1000 ms/giây / 60 FPS = 16.666 ms mỗi khung hình
1000 ms/giây / 56.25 FPS = 17.777 ms mỗi khung hình
Nếu ứng dụng của bạn chạy ở 900 FPS, điều này tương đương với thời gian khung hình là 1,111 mili giây mỗi khung hình. Ở 450 FPS, đây là 2,222 mili giây mỗi khung hình. Điều này thể hiện sự khác biệt chỉ 1,111 mili giây mỗi khung hình, mặc dù tốc độ khung hình dường như giảm đi một nửa.
Nếu bạn nhìn vào sự khác biệt giữa 60 FPS và 56.25 FPS, điều đó tương đương với 16,666 mili giây mỗi khung hình và 17,777 mili giây mỗi khung hình, tương ứng. Điều này cũng đại diện cho thêm 1,111 mili giây mỗi khung hình, nhưng ở đây, sự sụt giảm tốc độ khung hình có vẻ ít ấn tượng hơn về mặt phần trăm.
Đây là lý do tại sao các nhà phát triển sử dụng thời gian khung hình trung bình để đánh giá tốc độ trò chơi thay vì FPS.
Đừng lo lắng về FPS trừ khi bạn giảm xuống dưới tốc độ khung hình mục tiêu của mình. Tập trung vào thời gian khung hình để đo tốc độ trò chơi của bạn chạy, sau đó duy trì trong ngân sách khung hình của bạn.
Đọc bài báo gốc, “Robert Dunlop’s fps versus frame time,” để biết thêm thông tin.

FPS so với thời gian khung hình
Thử thách di động
Kiểm soát nhiệt là một trong những lĩnh vực quan trọng nhất cần tối ưu hóa khi phát triển các ứng dụng cho thiết bị di động. Nếu CPU hoặc GPU hoạt động hết công suất quá lâu do mã không hiệu quả, các chip đó sẽ bị nóng. Để tránh quá nhiệt và hư hỏng tiềm tàng cho chip, hệ điều hành sẽ giảm tốc độ xung nhịp của thiết bị để cho phép nó nguội đi, gây ra hiện tượng giật khung hình và trải nghiệm người dùng kém. Sự giảm hiệu suất này được gọi là giới hạn nhiệt.
Tốc độ khung hình cao hơn và việc tăng cường thực thi mã (hoặc các thao tác truy cập DRAM) dẫn đến tiêu thụ pin và sinh nhiệt tăng lên. Hiệu suất kém cũng có thể khiến trò chơi của bạn không thể chơi được trên toàn bộ các thiết bị di động cấp thấp, điều này có thể dẫn đến bỏ lỡ cơ hội thị trường.
Khi giải quyết vấn đề về nhiệt, hãy xem xét ngân sách bạn có như một ngân sách toàn hệ thống.
Chống quá nhiệt và hao pin bằng cách phân tích sớm để tối ưu hóa trò chơi của bạn ngay từ đầu. Thiết lập dự án của bạn cho phần cứng nền tảng mục tiêu để khắc phục các vấn đề về nhiệt và hao pin.
Điều chỉnh ngân sách khung trên di động
Một mẹo chung để chống lại các vấn đề nhiệt của thiết bị trong thời gian chơi kéo dài là để thời gian khung hình nhàn rỗi khoảng 35%. Điều này cho các chip di động thời gian để nguội và giúp ngăn ngừa hao pin quá mức. Sử dụng thời gian khung hình mục tiêu là 33,33 ms mỗi khung hình (cho 30 fps), ngân sách khung hình cho các thiết bị di động sẽ xấp xỉ 22 ms mỗi khung hình.
Phép tính trông như thế này: (1000 ms / 30) * 0.65 = 21.66 ms
Để đạt được 60 FPS trên thiết bị di động bằng cách sử dụng cùng một phép tính sẽ yêu cầu thời gian khung hình mục tiêu là (1000 ms / 60) * 0.65 = 10.83 ms. Điều này khó đạt được trên nhiều thiết bị di động và sẽ làm hao pin nhanh gấp đôi so với việc nhắm mục tiêu 30 fps. Vì những lý do này, nhiều trò chơi di động nhắm đến 30 FPS thay vì 60. Sử dụng Application.targetFrameRate để điều khiển cài đặt này, và tham khảo phần "Thiết lập ngân sách khung hình" trong sách điện tử về phân tích hiệu năng để biết thêm chi tiết về thời gian khung hình.
Điều chỉnh tần số trên chip di động có thể gây khó khăn trong việc xác định phân bổ ngân sách thời gian nhàn rỗi khung hình khi phân tích. Những cải tiến và tối ưu hóa của bạn có thể có tác động tích cực ròng, nhưng thiết bị di động có thể đang giảm tần số, và kết quả là chạy mát hơn. Sử dụng các công cụ tùy chỉnh như FTrace hoặc Perfetto để giám sát tần số chip di động, thời gian nhàn rỗi và khả năng mở rộng trước và sau khi tối ưu hóa.
Miễn là bạn giữ trong giới hạn tổng thời gian khung hình cho FPS mục tiêu của mình (ví dụ: 33,33 ms cho 30 FPS) và thấy thiết bị của bạn hoạt động ít hơn hoặc ghi lại nhiệt độ thấp hơn để duy trì tốc độ khung hình này, thì bạn đang đi đúng hướng.
Một lý do khác để thêm khoảng đệm vào ngân sách khung trên các thiết bị di động là để tính đến sự dao động nhiệt độ thực tế. Vào một ngày nóng, thiết bị di động sẽ bị nóng lên và gặp khó khăn trong việc tản nhiệt, điều này có thể dẫn đến hiện tượng giảm hiệu năng nhiệt (thermal throttling) và hiệu suất trò chơi kém. Dành một tỷ lệ phần trăm ngân sách khung hình để giúp tránh kịch bản này.

Theo dõi tần số CPU và các trạng thái nhàn rỗi bằng các công cụ như FTrace hoặc Perfetto để giúp xác định kết quả của các tối ưu hóa phân bổ ngân sách khung hình.
Giảm các thao tác truy cập bộ nhớ
Truy cập DRAM thường là một thao tác tiêu tốn nhiều năng lượng trên các thiết bị di động. Lời khuyên tối ưu hóa của ARM cho nội dung đồ họa trên thiết bị di động cho biết việc truy cập bộ nhớ LPDDR4 tốn khoảng 100 picojoule mỗi byte.
Giảm số lượng thao tác truy cập bộ nhớ trên mỗi khung hình bằng cách:
- Giảm tốc độ khung hình
- Giảm độ phân giải hiển thị nếu có thể
- Sử dụng các lưới đơn giản hơn với số lượng đỉnh và độ chính xác thuộc tính giảm
- Sử dụng nén kết cấu và mipmapping
Khi bạn cần tập trung vào các thiết bị sử dụng phần cứng CPU hoặc GPU của ARM, công cụ Arm Performance Studio (cụ thể là Streamline Performance Analyzer) bao gồm một số bộ đếm hiệu suất tuyệt vời để xác định các vấn đề về băng thông bộ nhớ. Các bộ đếm có sẵn được liệt kê và giải thích cho từng thế hệ GPU ARM trong hướng dẫn sử dụng tương ứng, ví dụ: Mali-G710 Performance Counter Reference Guide . Lưu ý rằng việc phân tích hồ sơ GPU của Arm Performance Studio yêu cầu GPU Arm Immortalis hoặc Mali.
Thiết lập các cấp độ phần cứng để đánh giá hiệu năng
Ngoài việc sử dụng các công cụ phân tích hiệu năng dành riêng cho nền tảng, hãy thiết lập các cấp độ hoặc thiết bị có cấu hình thấp nhất cho mỗi nền tảng và cấp độ chất lượng bạn muốn hỗ trợ, sau đó phân tích và tối ưu hóa hiệu suất cho từng cấu hình này.
Ví dụ, nếu bạn nhắm mục tiêu các nền tảng di động, bạn có thể quyết định hỗ trợ ba cấp độ với các kiểm soát chất lượng bật hoặc tắt các tính năng dựa trên phần cứng mục tiêu. Sau đó bạn tối ưu hóa cho thông số kỹ thuật thiết bị thấp nhất trong mỗi hạng. Ví dụ khác, nếu bạn đang phát triển một trò chơi cho máy chơi game, hãy đảm bảo bạn kiểm tra hiệu năng trên cả các phiên bản cũ và mới hơn.
Hướng dẫn tối ưu hóa di động mới nhất của chúng tôi có nhiều mẹo và thủ thuật sẽ giúp bạn giảm hiện tượng giảm hiệu năng do nhiệt và tăng thời lượng pin cho các thiết bị di động đang chạy trò chơi của bạn.

Arm’s Streamline Performance Analyzer bao gồm rất nhiều thông tin bộ đếm hiệu suất có thể được ghi lại trong các phiên phân tích trực tiếp trên phần cứng Arm mục tiêu. Điều này rất hữu ích để xác định các vấn đề về hiệu suất như bão hòa băng thông bộ nhớ do quá nhiều lần vẽ.
Từ phân tích hồ sơ cấp cao đến cấp thấp
Khi lập hồ sơ, bạn muốn đảm bảo rằng bạn tập trung thời gian và nỗ lực vào những lĩnh vực mà bạn có thể tạo ra tác động lớn nhất. Do đó, người ta khuyên nên bắt đầu bằng cách tiếp cận từ trên xuống dưới khi phân tích hiệu năng, nghĩa là bạn bắt đầu với cái nhìn tổng quan ở cấp cao về các danh mục như cấp phát kết xuất, tập lệnh, vật lý và thu gom rác (GC). Sau khi xác định được các lĩnh vực đáng lo ngại, bạn có thể đi sâu vào các chi tiết sâu hơn. Sử dụng lần quét cấp cao này để thu thập dữ liệu và ghi chú về các vấn đề hiệu suất quan trọng nhất, bao gồm các kịch bản gây ra phân bổ được quản lý không mong muốn hoặc sử dụng CPU quá mức trong vòng lặp trò chơi cốt lõi của bạn.
Bạn sẽ cần thu thập các call stack cho các dấu GC.Alloc trước. Nếu bạn không quen thuộc với quy trình này, hãy tìm một số mẹo và thủ thuật trong phần có tiêu đề "Tìm các cấp phát bộ nhớ lặp lại trong suốt vòng đời ứng dụng" trong sách điện tử.
Nếu các ngăn xếp cuộc gọi được báo cáo không đủ chi tiết để theo dõi nguồn gốc của các phân bổ hoặc các tình trạng chậm khác, bạn có thể thực hiện một phiên phân tích hồ sơ thứ hai với Deep Profiling được bật để tìm ra nguồn gốc của các phân bổ. Chúng tôi đề cập chi tiết hơn về hồ sơ sâu trong sách điện tử nhưng tóm lại, đó là một chế độ trong Profiler ghi lại dữ liệu hiệu suất chi tiết cho mọi lời gọi hàm, cung cấp những hiểu biết sâu sắc chi tiết về thời gian thực thi và hành vi, nhưng với chi phí hoạt động cao hơn đáng kể so với việc lập hồ sơ tiêu chuẩn.
Khi ghi chú về thời gian khung hình của những "kẻ vi phạm", hãy chắc chắn ghi chú cách chúng so sánh so với phần còn lại của khung hình. Tác động tương đối này có thể bị sai lệch khi bật hồ sơ sâu, bởi vì hồ sơ sâu thêm chi phí đáng kể bằng cách lập công cụ cho mọi lời gọi phương thức.
Hồ sơ sớm
Mặc dù bạn nên luôn lập hồ sơ trong suốt chu trình phát triển của dự án, những lợi ích đáng kể nhất từ việc lập hồ sơ được tạo ra khi bạn bắt đầu ở các giai đoạn đầu.
Hồ sơ sớm và thường xuyên để bạn và nhóm của bạn hiểu và ghi nhớ một "chữ ký hiệu suất" cho dự án mà bạn có thể sử dụng để đối chiếu. Nếu hiệu suất giảm mạnh, bạn sẽ dễ dàng phát hiện khi nào có vấn đề và khắc phục sự cố.
Trong khi việc lập hồ sơ trong Editor cung cấp cho bạn một cách dễ dàng để xác định các vấn đề chính, kết quả lập hồ sơ chính xác nhất luôn đến từ việc chạy và lập hồ sơ các bản dựng trên các thiết bị mục tiêu, cùng với việc tận dụng các công cụ dành riêng cho nền tảng để tìm hiểu các đặc điểm phần cứng của từng nền tảng. Sự kết hợp này sẽ cung cấp cho bạn cái nhìn toàn diện về hiệu suất ứng dụng trên tất cả các thiết bị mục tiêu của bạn. Ví dụ, bạn có thể bị giới hạn bởi GPU trên một số thiết bị di động nhưng lại bị giới hạn bởi CPU trên những thiết bị khác, và bạn chỉ có thể biết điều này bằng cách đo lường trên các thiết bị đó.
Xác định các vấn đề về hiệu suất
Tải xuống phiên bản PDF có thể in của biểu đồ này here.
Mục đích của việc lập hồ sơ là xác định các điểm nghẽn làm mục tiêu tối ưu hóa. Nếu bạn dựa vào phỏng đoán, bạn có thể tối ưu hóa các phần của trò chơi không phải là nút thắt cổ chai, dẫn đến cải thiện ít hoặc không có gì về hiệu suất tổng thể. Một số "tối ưu hóa" thậm chí có thể làm giảm hiệu suất tổng thể của trò chơi bạn, trong khi những tối ưu hóa khác có thể tốn nhiều công sức nhưng lại mang lại kết quả không đáng kể. Chìa khóa là tối ưu hóa tác động của việc đầu tư thời gian tập trung của bạn.
Sơ đồ luồng ở trên minh họa quy trình lập hồ sơ ban đầu với các phần sau cung cấp thông tin chi tiết về từng bước. Họ cũng trình bày các bản ghi Profiler từ các dự án Unity thực tế để minh họa các loại điều cần tìm.
Để có cái nhìn toàn diện về tất cả hoạt động của CPU, bao gồm cả thời điểm nó chờ GPU, hãy sử dụng Timeline view trong CPU module của Profiler. Hãy làm quen với các dấu hiệu Profiler phổ biến để diễn giải các bản ghi chính xác. Một số dấu hiệu của Profiler có thể hiển thị khác nhau tùy thuộc vào nền tảng mục tiêu của bạn, vì vậy hãy dành thời gian khám phá các bản ghi trò chơi của bạn trên từng nền tảng mục tiêu để cảm nhận xem một bản ghi "bình thường" cho dự án của bạn trông như thế nào.
Hiệu suất của một dự án bị giới hạn bởi chip và/hoặc luồng mất nhiều thời gian nhất. Đó là khu vực mà các nỗ lực tối ưu hóa nên tập trung. Ví dụ, hãy tưởng tượng các kịch bản sau cho một trò chơi với ngân sách thời gian khung hình mục tiêu là 33,33 ms và VSync được bật:
- Nếu thời gian khung hình của CPU (không bao gồm VSync) là 25 ms và thời gian của GPU là 20 ms, không có vấn đề gì! Bạn bị giới hạn bởi CPU, nhưng mọi thứ đều trong ngân sách, và tối ưu hóa mọi thứ sẽ không cải thiện tốc độ khung hình (trừ khi bạn đưa cả CPU và GPU xuống dưới 16,66 ms và tăng lên 60 fps).
- Nếu thời gian khung CPU là 40 ms và GPU là 20 ms, bạn đang bị giới hạn bởi CPU và sẽ cần tối ưu hóa hiệu suất CPU. Tối ưu hóa hiệu suất GPU sẽ không giúp ích; trên thực tế, bạn có thể muốn chuyển một phần công việc của CPU sang GPU, ví dụ, bằng cách sử dụng compute shaders thay vì mã C# khi có thể, để cân bằng mọi thứ.
- Nếu thời gian khung CPU là 20 ms và GPU là 40 ms, bạn đang bị giới hạn bởi GPU và cần tối ưu hóa công việc của GPU.
- Nếu CPU và GPU đều ở mức 40 ms, bạn bị giới hạn bởi cả hai và sẽ cần tối ưu cả hai xuống dưới 33.33 ms để đạt được 30 fps.
Xem các tài nguyên này để tìm hiểu sâu hơn về việc bị giới hạn bởi CPU hoặc GPU:

Làm theo lưu đồ này và sử dụng Profiler để giúp xác định nơi cần tập trung nỗ lực tối ưu hóa của bạn.
Bạn có nằm trong ngân sách khung không?
Việc lập hồ sơ và tối ưu hóa dự án của bạn sớm và thường xuyên trong suốt quá trình phát triển sẽ giúp bạn đảm bảo rằng tất cả các luồng CPU và thời gian khung hình GPU tổng thể của ứng dụng đều nằm trong ngân sách khung hình. Câu hỏi sẽ hướng dẫn quy trình này là, bạn có nằm trong ngân sách khung hay không?
Phía trên là hình ảnh chụp hồ sơ từ một trò chơi di động Unity được phát triển bởi một đội ngũ đã tiến hành phân tích và tối ưu hóa liên tục. Trò chơi nhắm đến 60 FPS trên điện thoại di động cấu hình cao và 30 FPS trên điện thoại cấu hình trung bình/thấp, như chiếc trong ảnh chụp này.
Lưu ý rằng gần một nửa thời gian trên khung hình được chọn bị chiếm bởi dấu hiệu Profiler màu vàng WaitForTargetFPS. Ứng dụng đã đặt Application.targetFrameRate thành 30 FPS, và VSync được bật. Công việc xử lý thực tế trên luồng chính hoàn thành vào khoảng mốc 19 ms, và phần thời gian còn lại được dành để chờ phần còn lại của 33,33 ms trôi qua trước khi bắt đầu khung hình tiếp theo. Mặc dù thời gian này được biểu thị bằng dấu hiệu Profiler, luồng CPU chính về cơ bản là không hoạt động trong thời gian này, cho phép CPU nguội đi và sử dụng lượng pin tối thiểu.
Dấu hiệu cần chú ý có thể khác trên các nền tảng khác hoặc nếu VSync bị tắt. Điều quan trọng là phải kiểm tra xem luồng chính có đang chạy trong giới hạn khung hình của bạn hay chính xác trên giới hạn khung hình của bạn với một loại dấu hiệu nào đó cho biết ứng dụng đang chờ VSync và liệu các luồng khác có thời gian nhàn rỗi nào không.
Thời gian nhàn rỗi được biểu thị bằng các dấu hiệu màu xám hoặc màu vàng của Profiler. Ảnh chụp màn hình ở trên cho thấy luồng render đang nhàn rỗi trong Gfx.WaitForGfxCommandsFromMainThread, điều này cho thấy những thời điểm nó đã gửi xong các lệnh vẽ đến GPU trong một khung hình và đang chờ thêm các yêu cầu lệnh vẽ từ CPU trong khung hình tiếp theo. Tương tự, mặc dù luồng Job Worker 0 dành một ít thời gian trong Canvas.GeometryJob, hầu hết thời gian nó đều nhàn rỗi. Tất cả những điều này đều là dấu hiệu của một ứng dụng nằm thoải mái trong ngân sách khung.
Nếu trò chơi của bạn nằm trong ngân sách khung hình
Nếu bạn nằm trong ngân sách khung, bao gồm bất kỳ điều chỉnh nào được thực hiện đối với ngân sách để tính đến việc sử dụng pin và giới hạn nhiệt, bạn đã hoàn thành các tác vụ phân tích khóa. Bạn có thể kết luận bằng cách chạy Memory Profiler để đảm bảo rằng ứng dụng cũng nằm trong ngân sách bộ nhớ của nó.
Hình ảnh trên cho thấy một trò chơi chạy thoải mái trong ngân sách khung hình ~22 ms cần thiết cho 30 FPS. Lưu ý khoảng đệm WaitForTargetfps làm đầy thời gian luồng chính cho đến VSync và các khoảng thời gian nhàn rỗi màu xám trong luồng kết xuất và luồng công nhân. Cũng lưu ý rằng khoảng thời gian VBlank có thể được quan sát bằng cách xem thời gian kết thúc của Gfx.Present frame qua từng khung hình, và bạn có thể vẽ một thang thời gian trong khu vực Timeline hoặc trên thanh thời gian ở trên cùng để đo từ cái này đến cái khác.

Đây là hồ sơ của một trò chơi chạy thoải mái trong ngân sách khung hình ~22 ms cần thiết cho 30 FPS mà không bị quá nhiệt. Lưu ý khoảng đệm WaitForTargetfps làm đầy thời gian luồng chính cho đến VSync và các khoảng thời gian nhàn rỗi màu xám trong luồng kết xuất và luồng công nhân. Cũng lưu ý rằng khoảng thời gian VBlank có thể được quan sát bằng cách xem thời gian kết thúc của Gfx.Present frame qua từng khung hình, và bạn có thể vẽ một thang thời gian trong chế độ xem Timeline hoặc trên thanh đo thời gian ở trên cùng, để đo từ cái này đến cái khác.
Bị giới hạn bởi CPU
Nếu trò chơi của bạn không nằm trong ngân sách khung hình của CPU, bước tiếp theo là điều tra phần nào của CPU đang là nút thắt cổ chai – nói cách khác, luồng nào đang bận nhất.
Hiếm khi toàn bộ khối lượng công việc của CPU là nút thắt cổ chai. Các CPU hiện đại có một số lõi khác nhau, có khả năng thực hiện công việc một cách độc lập và đồng thời. Các luồng khác nhau có thể chạy trên mỗi lõi CPU. Một ứng dụng Unity hoàn chỉnh sử dụng một loạt các luồng cho các mục đích khác nhau, nhưng những luồng phổ biến nhất để tìm các vấn đề về hiệu suất là:
- Luồng chính: Đây là nơi phần lớn logic/script của trò chơi thực hiện công việc của chúng theo mặc định. Hầu hết các hệ thống Unity, chẳng hạn như vật lý, hoạt ảnh, UI và các giai đoạn ban đầu của việc kết xuất, được thực thi ở đây.
- Luồng kết xuất: Điều này xử lý công việc chuẩn bị (ví dụ: những đối tượng nào trong cảnh có thể nhìn thấy được bởi camera và những đối tượng nào bị loại trừ/vô hình vì chúng nằm ngoài hình nón nhìn, bị che khuất, hoặc bị loại bỏ theo các tiêu chí khác) mà phải xảy ra trước khi gửi các lệnh kết xuất đến GPU.
- Trong quá trình kết xuất, luồng chính kiểm tra cảnh và thực hiện loại bỏ camera, sắp xếp độ sâu và nhóm lệnh vẽ, tạo ra một danh sách các đối tượng cần kết xuất. Danh sách này được truyền đến luồng kết xuất, nơi nó dịch nó từ biểu diễn không phụ thuộc nền tảng nội bộ của Unity sang các lệnh API đồ họa cụ thể cần thiết để hướng dẫn GPU trên một nền tảng cụ thể.
- Các luồng công nhân công việc: Các nhà phát triển có thể sử dụng hệ thống công việc để lên lịch cho các loại công việc nhất định chạy trên các luồng công nhân, điều này làm giảm khối lượng công việc trên luồng chính. Một số hệ thống và tính năng của Unity cũng sử dụng hệ thống tác vụ, chẳng hạn như vật lý, hoạt ảnh và kết xuất.
Một ví dụ thực tế về tối ưu hóa luồng chính
Hình ảnh dưới đây cho thấy mọi thứ có thể trông như thế nào trong một dự án bị ràng buộc bởi luồng chính. Dự án này đang chạy trên Meta Quest 2, vốn thường nhắm mục tiêu ngân sách khung hình là 13,88 ms (72 FPS) hoặc thậm chí 8,33 ms (120 FPS), vì tốc độ khung hình cao là quan trọng để tránh say tàu xe trong các thiết bị VR. Tuy nhiên, ngay cả khi trò chơi này nhắm đến 30 FPS, rõ ràng dự án này đang gặp khó khăn.
Mặc dù luồng kết xuất và các luồng công nhân trông giống với ví dụ nằm trong ngân sách khung hình, luồng chính rõ ràng bận rộn với công việc trong suốt khung hình. Ngay cả khi tính đến lượng chi phí nhỏ của trình phân tích hiệu năng ở cuối khung hình, luồng chính vẫn bận rộn hơn 45 ms, nghĩa là dự án này đạt tốc độ khung hình dưới 22 FPS. Không có dấu hiệu nào cho thấy luồng chính đang chờ VSync một cách nhàn rỗi; nó bận trong suốt khung hình.
Giai đoạn điều tra tiếp theo là xác định các bộ phận của khung mất nhiều thời gian nhất và tìm hiểu lý do tại sao lại như vậy. Trong khung hình này, PostLateUpdate.FinishFrameRendering mất 16.23 ms, nhiều hơn toàn bộ ngân sách khung hình. Kiểm tra kỹ hơn cho thấy có năm trường hợp của một dấu hiệu gọi là Inl_RenderCameraStack, cho thấy năm camera đang hoạt động và kết xuất cảnh. Vì mọi camera trong Unity đều kích hoạt toàn bộ pipeline kết xuất, bao gồm cả việc loại bỏ, sắp xếp và gộp, nhiệm vụ ưu tiên cao nhất cho dự án này là giảm số lượng camera đang hoạt động, lý tưởng nhất là chỉ còn một.
BehaviourUpdate, dấu hiệu của Profiler bao gồm tất cả các lần thực thi phương thức MonoBehaviour.Update() mất 7,27 mili giây trong khung hình này.
Trong chế độ xem Timeline, các phần có màu magenta cho biết các điểm mà các script đang cấp phát bộ nhớ heap được quản lý. Chuyển sang chế độ xem Hierarchy, và lọc bằng cách gõ GC.Alloc trong thanh tìm kiếm, cho thấy việc cấp phát bộ nhớ này mất khoảng 0.33 ms trong khung hình này. Tuy nhiên, đó là một phép đo không chính xác về tác động của việc cấp phát bộ nhớ đối với hiệu suất CPU của bạn.
Các dấu thời gian GC.Alloc không được đo bằng cách ghi lại điểm Bắt đầu và Kết thúc như các mẫu Profiler thông thường. Để giảm thiểu chi phí hoạt động, Unity chỉ ghi lại dấu thời gian của việc cấp phát và kích thước được cấp phát.
Trình hồ sơ gán một thời lượng mẫu nhân tạo nhỏ cho các dấu GC.Alloc chỉ để đảm bảo chúng hiển thị trong các chế độ xem của Trình hồ sơ. Việc cấp phát thực tế có thể mất nhiều thời gian hơn, đặc biệt nếu cần yêu cầu một phạm vi bộ nhớ mới từ hệ thống. Để thấy rõ hơn tác động, hãy đặt các dấu hiệu Profiler xung quanh đoạn mã thực hiện việc cấp phát; trong quá trình phân tích sâu, các khoảng trống giữa các mẫu GC.Alloc màu magenta trong chế độ xem Timeline cung cấp một số chỉ báo về thời gian chúng có thể đã mất.
Ngoài ra, việc cấp phát bộ nhớ mới có thể có những tác động tiêu cực đến hiệu suất mà khó đo lường và quy kết trực tiếp:
- Yêu cầu bộ nhớ mới từ hệ thống có thể ảnh hưởng đến ngân sách điện năng trên thiết bị di động, điều này có thể khiến hệ thống làm chậm CPU hoặc GPU.
- Bộ nhớ mới có thể cần được tải vào Bộ nhớ đệm L1 của CPU và do đó đẩy các dòng bộ nhớ đệm hiện có ra ngoài.
- Thu gom rác tăng dần hoặc đồng bộ có thể được kích hoạt trực tiếp hoặc với độ trễ khi không gian trống hiện có trong Bộ nhớ được Quản lý cuối cùng bị vượt quá.
Vào đầu khung hình, bốn lần gọi Physics.FixedUpdate cộng lại là 4.57 ms. Sau đó, LateBehaviourUpdate (các lời gọi đến MonoBehaviour.LateUpdate()) mất 4 ms, và Animators chiếm khoảng 1 ms. Để đảm bảo dự án này đạt được ngân sách và tỷ lệ khung hình mong muốn, tất cả các vấn đề luồng chính này cần được điều tra để tìm các tối ưu hóa phù hợp.
Những cạm bẫy phổ biến gây tắc nghẽn luồng chính
Những cải thiện hiệu suất lớn nhất sẽ đạt được bằng cách tối ưu hóa những thứ tốn nhiều thời gian nhất. Các lĩnh vực sau đây thường là những nơi màu mỡ để tìm kiếm tối ưu hóa trong các dự án bị ràng buộc bởi luồng chính:
- Các phép tính vật lý
- Cập nhật script MonoBehaviour
- Phân bổ và/hoặc thu thập rác
- Loại bỏ và kết xuất camera trên luồng chính
- Gộp lệnh vẽ không hiệu quả
- Cập nhật giao diện người dùng, bố cục và xây dựng lại
- Hoạt hình
Đọc các hướng dẫn tối ưu hóa của chúng tôi cung cấp danh sách dài các mẹo hành động để tối ưu hóa một số cạm bẫy phổ biến nhất:
Tùy thuộc vào vấn đề bạn muốn điều tra, các công cụ khác cũng có thể hữu ích:
- Đối với các script MonoBehaviour mất nhiều thời gian nhưng không cho bạn biết chính xác lý do, hãy thêm Profiler Markers vào mã hoặc thử deep profiling để xem toàn bộ ngăn xếp cuộc gọi.
- Đối với các tập lệnh cấp phát bộ nhớ được quản lý, hãy bật Allocation Call Stacks để xem chính xác nơi các lần cấp phát đến từ. Hoặc, bật hồ sơ sâu hoặc sử dụng Project Auditor, cái này hiển thị các vấn đề mã được lọc theo bộ nhớ, để bạn có thể xác định tất cả các dòng mã dẫn đến các cấp phát được quản lý.
- Sử dụng Frame Debugger để điều tra nguyên nhân của việc nhóm lệnh vẽ kém.
Để có các mẹo toàn diện về tối ưu hóa trò chơi của bạn, hãy tải xuống các hướng dẫn chuyên gia Unity miễn phí này:

Chụp từ một dự án được ràng buộc với luồng chính
Giới hạn bởi CPU: Luồng kết xuất
Đây là một dự án thực tế bị ràng buộc bởi luồng render của nó. Đây là một trò chơi console với góc nhìn isometric và ngân sách khung hình mục tiêu là 33,33 ms.
Bản ghi của Profiler cho thấy trước khi việc kết xuất có thể bắt đầu trên khung hình hiện tại, luồng chính chờ luồng kết xuất, như được chỉ ra bởi dấu Gfx.WaitForPresentOnGfxThread . Luồng render vẫn đang gửi các lệnh gọi vẽ từ khung hình trước và chưa sẵn sàng nhận các lệnh gọi vẽ mới từ luồng chính; nó cũng đang dành thời gian trong Camera.Render.
Bạn có thể phân biệt được các dấu hiệu liên quan đến khung hình hiện tại và các dấu hiệu từ các khung hình khác, bởi vì những dấu hiệu sau có vẻ tối hơn. Bạn cũng có thể thấy rằng một khi luồng chính có thể tiếp tục và bắt đầu gửi các lệnh vẽ cho luồng kết xuất xử lý, luồng kết xuất mất hơn 100 ms để xử lý khung hình hiện tại, điều này cũng tạo ra nút cổ chai trong khung hình tiếp theo.
Điều tra thêm cho thấy trò chơi này có một thiết lập kết xuất phức tạp, liên quan đến chín camera khác nhau và nhiều lần chạy phụ do các shader thay thế. Trò chơi cũng đang kết xuất hơn 130 điểm sáng bằng cách sử dụng đường dẫn kết xuất tiến, điều này có thể thêm nhiều lệnh vẽ trong suốt bổ sung cho mỗi ánh sáng. Tổng cộng, những vấn đề này kết hợp lại tạo ra hơn 3000 lệnh gọi vẽ mỗi khung hình.
Những cạm bẫy phổ biến gây tắc nghẽn luồng render
Các nguyên nhân phổ biến cần điều tra đối với các dự án bị giới hạn bởi luồng kết xuất:
- Lô gọi vẽ kém: Điều này đặc biệt áp dụng đối với các API đồ họa cũ hơn như OpenGL hoặc DirectX 11.
- Quá nhiều camera: Trừ khi bạn đang tạo một trò chơi Multiplayer màn hình chia đôi, khả năng là bạn chỉ nên có một Camera hoạt động.
- Cắt tỉa kém: Điều này dẫn đến việc vẽ quá nhiều thứ. Điều tra các kích thước hình nón của Camera và các mặt nạ lớp cắt.
Module Hồ sơ Kết xuất hiển thị tổng quan về số lượng lô lệnh vẽ và các lệnh SetPass mỗi khung hình. Công cụ tốt nhất để điều tra các lô lệnh vẽ mà luồng kết xuất của bạn đang gửi đến GPU là Frame Debugger.
Các công cụ để giải quyết các nút thắt cổ chai đã xác định
Trong khi trọng tâm của cuốn sách điện tử này là về việc xác định các vấn đề về hiệu suất, hai hướng dẫn tối ưu hóa hiệu suất bổ sung mà chúng tôi đã nêu bật trước đó đưa ra các gợi ý về cách giải quyết các điểm nghẽn, tùy thuộc vào việc nền tảng mục tiêu của bạn là PC hay máy console hay di động. Trong bối cảnh các nút thắt cổ chai của luồng render, điều đáng nhấn mạnh là Unity cung cấp các hệ thống và tùy chọn gom nhóm khác nhau tùy thuộc vào các vấn đề bạn đã xác định. Dưới đây là tổng quan nhanh về một số tùy chọn mà chúng tôi giải thích chi tiết hơn trong sách điện tử:
- SRP Batching giảm chi phí CPU bằng cách lưu trữ dữ liệu vật liệu một cách bền vững trong bộ nhớ GPU. Mặc dù nó không giảm số lượng lệnh vẽ thực tế, nó làm cho mỗi lệnh vẽ rẻ hơn.
- GPU instancing kết hợp nhiều thể hiện của cùng một lưới bằng cách sử dụng cùng một vật liệu thành một lệnh vẽ duy nhất.
- Static Batching kết hợp các lưới tĩnh (không di chuyển) chia sẻ cùng một vật liệu và do đó có thể mang lại cho bạn lợi thế khi làm việc với một thiết kế cấp độ có nhiều yếu tố tĩnh.
- GPU resident drawer tự động sử dụng tính năng nhân bản GPU để giảm chi phí CPU và số lần gọi vẽ, bằng cách nhóm các GameObjects tương tự lại với nhau.
- Dynamic Batching kết hợp các lưới nhỏ tại thời điểm chạy, điều này có thể là một lợi thế trên các thiết bị di động cũ có chi phí gọi vẽ cao. Tuy nhiên, nhược điểm là phép biến đổi đỉnh cũng có thể tốn tài nguyên.
- GPU occlusion culling sử dụng các shader tính toán để xác định khả năng hiển thị của đối tượng bằng cách so sánh các bộ đệm độ sâu từ khung hình hiện tại và khung hình trước đó, giảm việc kết xuất không cần thiết các đối tượng bị che khuất mà không cần dữ liệu được nướng trước.
Ngoài ra, ở phía CPU, các kỹ thuật như Camera.layerCullDistances có thể được sử dụng để giảm số lượng đối tượng được gửi đến luồng kết xuất bằng cách loại bỏ các đối tượng dựa trên khoảng cách của chúng so với camera, giúp giảm bớt tắc nghẽn CPU trong quá trình loại bỏ camera.
Đây chỉ là một số lựa chọn có sẵn. Mỗi cái trong số này đều có những ưu điểm và nhược điểm khác nhau. Một số bị giới hạn ở các nền tảng nhất định. Các dự án thường cần sử dụng kết hợp của một số hệ thống này và để làm được điều đó, cần có sự hiểu biết về cách tận dụng tối đa chúng.

Một kịch bản bị ràng buộc bởi luồng Render
Giới hạn bởi CPU: Luồng công nhân
Các dự án bị ràng buộc bởi các luồng CPU khác ngoài luồng chính hoặc luồng kết xuất không phổ biến lắm. Tuy nhiên, điều này có thể xảy ra nếu dự án của bạn sử dụng Data-Oriented Technology Stack (DOTS), đặc biệt nếu công việc được chuyển khỏi luồng chính sang các luồng công nhân bằng cách sử dụng job system.
Hình ảnh trên là một ảnh chụp từ chế độ Play trong Editor, cho thấy một dự án DOTS đang chạy mô phỏng chất lỏng hạt trên CPU.
Trông có vẻ thành công thoạt nhìn. Các luồng công nhân được đóng gói chặt chẽ với các công việc Burst-compiled, cho thấy một lượng lớn công việc đã được chuyển khỏi luồng chính. Thông thường, đây là một quyết định đúng đắn.
Tuy nhiên, trong trường hợp này, thời gian khung hình 48,14 ms và dấu WaitForJobGroupID màu xám 35,57 ms trên luồng chính là những dấu hiệu cho thấy mọi thứ không ổn. WaitForJobGroupID cho biết luồng chính đã lên lịch các tác vụ để chạy bất đồng bộ trên các luồng công nhân, nhưng nó cần kết quả của các tác vụ đó trước khi các luồng công nhân hoàn thành việc chạy chúng. Các dấu hiệu Profiler màu xanh lam bên dưới WaitForJobGroupID cho thấy luồng chính đang chạy các tác vụ trong khi chờ đợi, nhằm cố gắng đảm bảo các tác vụ hoàn thành sớm hơn.
Mặc dù các công việc được biên dịch Burst, chúng vẫn đang làm rất nhiều việc. Có lẽ cấu trúc truy vấn không gian được dự án này sử dụng để nhanh chóng tìm các hạt gần nhau nên được tối ưu hóa hoặc thay thế bằng một cấu trúc hiệu quả hơn. Hoặc, các tác vụ truy vấn không gian có thể được lên lịch vào cuối khung hình thay vì đầu, với kết quả không cần thiết cho đến khi bắt đầu khung hình tiếp theo. Có lẽ dự án này đang cố gắng mô phỏng quá nhiều hạt. Cần phân tích sâu hơn mã của các công việc để tìm ra giải pháp, vì vậy việc thêm các dấu hiệu Profiler chi tiết hơn có thể giúp xác định các phần chậm nhất của chúng.
Các công việc trong dự án của bạn có thể không được song song hóa như trong ví dụ này. Có lẽ bạn chỉ có một công việc dài đang chạy trong một luồng worker duy nhất. Điều này ổn, miễn là khoảng thời gian giữa lúc công việc được lên lịch và lúc nó cần được hoàn thành đủ dài để công việc chạy. Nếu không, bạn sẽ thấy luồng chính bị đình trệ khi nó chờ công việc hoàn thành, như trong ảnh chụp màn hình ở trên.
Những cạm bẫy phổ biến gây tắc nghẽn luồng công nhân
Các nguyên nhân phổ biến gây ra các điểm đồng bộ và tắc nghẽn luồng công nhân bao gồm:
- Các tác vụ không được biên dịch bởi trình biên dịch Burst
- Các tác vụ chạy dài trên một luồng worker thay vì được song song hóa trên nhiều luồng worker
- Thời gian không đủ giữa thời điểm trong khung hình khi một công việc được lên lịch và thời điểm kết quả được yêu cầu
- Nhiều "điểm đồng bộ" trong một khung hình, yêu cầu tất cả các tác vụ phải hoàn thành ngay lập tức
Bạn có thể sử dụng tính năng Flow Events trong chế độ xem Timeline của module CPU Usage Profiler để điều tra khi nào các tác vụ được lên lịch và khi nào kết quả của chúng được mong đợi bởi luồng chính.
Để biết thêm thông tin về cách viết mã DOTS hiệu quả, hãy xem hướng dẫn Các thực hành tốt nhất về DOTS.

Một dự án dựa trên DOTS, nặng về mô phỏng, bị ràng buộc bởi các luồng công nhân
Bị giới hạn bởi GPU
Ứng dụng của bạn bị giới hạn bởi GPU nếu luồng chính dành nhiều thời gian trong các dấu hiệu hồ sơ như Gfx.WaitForPresentOnGfxThread, và luồng kết xuất của bạn đồng thời hiển thị các dấu hiệu như Gfx.PresentFrame hoặc .WaitForLastPresent.
Cách tốt nhất để lấy thời gian khung hình GPU là sử dụng công cụ phân tích GPU dành riêng cho nền tảng mục tiêu, nhưng không phải tất cả các thiết bị đều giúp việc thu thập dữ liệu đáng tin cậy trở nên dễ dàng.
API FrameTimingManager có thể hữu ích trong những trường hợp đó, cung cấp thời gian khung hình cấp cao, chi phí thấp cả trên CPU và GPU.
Ảnh chụp trên được thực hiện trên điện thoại di động Android bằng API đồ họa Vulkan. Mặc dù một phần thời gian dành cho Gfx.PresentFrame trong ví dụ này có thể liên quan đến việc chờ VSync, độ dài cực lớn của dấu hiệu Profiler này cho thấy phần lớn thời gian này là do chờ GPU hoàn thành việc kết xuất khung hình trước đó.
Trong trò chơi này, một số sự kiện trò chơi đã kích hoạt việc sử dụng một shader làm tăng gấp ba số lượng lệnh vẽ được GPU kết xuất. Các vấn đề phổ biến cần điều tra khi phân tích hiệu suất GPU bao gồm:
- Các hiệu ứng hậu kỳ toàn màn hình đắt tiền, như Ambient Occlusion và Bloom
- Các shader fragment đắt đỏ do:
- Logic phân nhánh bên trong mã shader
- Sử dụng độ chính xác float đầy đủ thay vì độ chính xác nửa, đặc biệt trên thiết bị di động
- Việc sử dụng quá mức các thanh ghi, ảnh hưởng đến việc chiếm dụng sóng trước của GPU
- Vẽ chồng lên nhau trong hàng đợi kết xuất trong suốt do:
- Kết xuất giao diện người dùng kém hiệu quả
- Sử dụng quá nhiều hoặc chồng chéo các hệ thống hạt
- Các hiệu ứng hậu kỳ
- Độ phân giải màn hình quá cao, chẳng hạn như:
- Màn hình 4K
- Màn hình Retina trên thiết bị di động
- Các tam giác nhỏ do:
- Hình học lưới dày đặc
- Thiếu hệ thống Mức độ chi tiết (LOD), đây là một vấn đề đặc biệt trên GPU di động, nhưng cũng có thể ảnh hưởng đến GPU PC và console.
- Lỗi bộ nhớ đệm và băng thông bộ nhớ GPU bị lãng phí do:
- Các kết cấu không nén
- Các kết cấu độ phân giải cao không có mipmap
- Shader hình học hoặc shader lát gạch, có thể chạy nhiều lần mỗi khung hình nếu bật đổ bóng động
Nếu ứng dụng của bạn có vẻ bị giới hạn bởi GPU, bạn có thể sử dụng Frame Debugger như một cách nhanh chóng để hiểu các lô lệnh vẽ đang được gửi đến GPU. Tuy nhiên, công cụ này không thể trình bày bất kỳ thông tin thời gian GPU cụ thể nào, mà chỉ cho biết cách cảnh tổng thể được xây dựng.
Cách tốt nhất để điều tra nguyên nhân gây ra tắc nghẽn GPU là kiểm tra bản ghi GPU từ một trình phân tích GPU phù hợp. Công cụ bạn sử dụng phụ thuộc vào phần cứng mục tiêu và API đồ họa được chọn. Xem phần công cụ phân tích và gỡ lỗi trong sách điện tử để biết thêm thông tin.

Ảnh chụp màn hình từ một trò chơi di động bị giới hạn bởi GPU
Thêm mẹo cho các nhà phát triển và người sáng tạo Unity
Tìm thêm các phương pháp hay nhất và mẹo từ trung tâm thực hành tốt nhất của Unity. Chọn từ hơn 30 hướng dẫn, được tạo bởi các chuyên gia trong ngành, các kỹ sư Unity và các nghệ sĩ kỹ thuật, để giúp bạn phát triển hiệu quả với các bộ công cụ và hệ thống của Unity.
