Nói chung mức độ thường xuyên và khi nào tôi nên tối ưu hóa mã của mình?


13

Trong bước tối ưu hóa lập trình kinh doanh 'bình thường' thường được để lại cho đến khi thực sự cần thiết. Có nghĩa là bạn không nên tối ưu hóa cho đến khi nó thực sự cần thiết.

Hãy nhớ những gì Donald Knuth đã nói "Chúng ta nên quên đi những hiệu quả nhỏ, nói về 97% thời gian: tối ưu hóa sớm là gốc rễ của mọi tội lỗi"

Khi nào là thời gian để tối ưu hóa để đảm bảo tôi không lãng phí công sức. Tôi có nên làm nó một cấp độ phương pháp? Đẳng cấp? Cấp mô-đun?

Ngoài ra những gì tôi nên đo lường tối ưu hóa? Bọ ve? Tỷ lệ khung hình? Tổng thời gian?

Câu trả lời:


18

Nơi tôi đã làm việc, chúng tôi luôn sử dụng nhiều cấp độ hồ sơ; nếu bạn thấy một vấn đề, bạn chỉ cần di chuyển xuống danh sách một chút nữa cho đến khi bạn biết được chuyện gì đang xảy ra:

  • "Hồ sơ con người", hay chỉ chơi trò chơi ; đôi khi nó cảm thấy chậm hoặc "quá giang"? Nhận thấy hình ảnh động giật? (Là nhà phát triển, lưu ý rằng bạn sẽ nhạy cảm hơn với một số loại vấn đề về hiệu suất và không biết gì về người khác. Lập kế hoạch thử nghiệm bổ sung cho phù hợp.)
  • Bật màn hình FPS , là cửa sổ FPS trung bình trượt 5 giây. Rất ít chi phí để tính toán và hiển thị.
  • Bật các thanh hồ sơ , chỉ là một loạt các hình tứ giác (màu ROYGBIV) đại diện cho các phần khác nhau của khung (ví dụ: vblank, preframe, cập nhật, va chạm, kết xuất, postframe) bằng cách sử dụng bộ đếm thời gian "đồng hồ bấm giờ" đơn giản xung quanh mỗi phần của mã . Để nhấn mạnh những gì chúng tôi muốn, chúng tôi đặt thanh có giá trị chiều rộng màn hình là đại diện cho khung mục tiêu 60Hz, vì vậy thật dễ dàng để xem bạn có ví dụ như ngân sách 50% (chỉ bằng một nửa thanh) hoặc hơn 50% ( thanh kết thúc tốt đẹp và trở thành một thanh rưỡi). Cũng khá dễ dàng để nói những gì thường ăn hầu hết các khung: red = render, yellow = update, v.v ...
  • Xây dựng một công cụ cụ thể đặc biệt chèn "đồng hồ bấm giờ" như mã xung quanh mỗi chức năng. (Lưu ý rằng bạn có thể đạt hiệu năng lớn, dcache và icache khi thực hiện việc này, vì vậy nó chắc chắn xâm phạm. Nhưng nếu bạn thiếu một trình tạo mẫu thích hợp hoặc hỗ trợ tốt trên CPU, đây là một lựa chọn chấp nhận được. về việc ghi lại tối thiểu dữ liệu về chức năng nhập / thoát và xây dựng lại các calltraces sau.) Khi chúng tôi xây dựng chúng tôi, chúng tôi đã bắt chước nhiều định dạng đầu ra của gprof .
  • Tốt nhất của tất cả, chạy một hồ sơ lấy mẫu ; VTune và CodeAnalyst có sẵn cho x86 và x64, bạn đã có nhiều môi trường mô phỏng hoặc mô phỏng khác nhau có thể cung cấp cho bạn dữ liệu ở đây.

(Có một câu chuyện thú vị từ một GDC năm ngoái của một lập trình viên đồ họa đã chụp bốn bức ảnh của anh ta - vui vẻ, thờ ơ, khó chịu và tức giận - và hiển thị một hình ảnh phù hợp ở góc của các bản dựng nội bộ dựa trên khung hình. những người sáng tạo nội dung đã nhanh chóng học được cách không bật các shader phức tạp cho tất cả các đối tượng và môi trường của họ: chúng sẽ khiến lập trình viên tức giận. Kìa sức mạnh của phản hồi.)

Lưu ý rằng bạn cũng có thể thực hiện những điều thú vị như vẽ biểu đồ "thanh hồ sơ" liên tục, do đó bạn có thể thấy các mẫu tăng đột biến ("chúng tôi đang mất một khung hình cứ sau 7 khung hình") hoặc tương tự.

Tuy nhiên, để trả lời câu hỏi của bạn: theo kinh nghiệm của tôi, trong khi nó hấp dẫn (và thường bổ ích - tôi thường học một cái gì đó) để viết lại các chức năng / mô-đun đơn lẻ để tối ưu hóa số lượng hướng dẫn hoặc hiệu suất icache hoặc dcache, và chúng tôi thực sự cần phải làm điều này đôi khi khi chúng ta gặp vấn đề về hiệu suất đặc biệt đáng ghét, phần lớn các vấn đề về hiệu suất mà chúng ta thường xuyên phải giải quyết là do thiết kế . Ví dụ:

  • Chúng ta có nên lưu trữ trong RAM hoặc tải lại từ đĩa khung hoạt hình trạng thái "tấn công" cho trình phát không? Làm thế nào về mỗi kẻ thù? Chúng tôi không có RAM để làm tất cả, nhưng tải đĩa rất tốn kém! Bạn có thể thấy quá giang nếu 5 hoặc 6 kẻ thù khác nhau xuất hiện cùng một lúc! (Được rồi, làm thế nào về sinh sản đáng kinh ngạc?)
  • Chúng ta đang thực hiện một loại hoạt động trên tất cả các hạt hay tất cả các hoạt động trên một hạt? (Đây là một sự đánh đổi icache / dcache và câu trả lời không phải lúc nào cũng rõ ràng.) Làm thế nào về việc tách tất cả các hạt và lưu trữ các vị trí lại với nhau ("cấu trúc mảng" nổi tiếng) so với việc giữ tất cả dữ liệu hạt ở một nơi (" mảng cấu trúc ").

Bạn nghe thấy nó cho đến khi nó trở nên đáng ghét trong bất kỳ khóa học khoa học máy tính cấp đại học nào, nhưng: nó thực sự là tất cả về cấu trúc dữ liệu và thuật toán. Dành một chút thời gian cho thuật toán và thiết kế luồng dữ liệu sẽ giúp bạn có nhiều tiếng vang hơn cho công việc nói chung. (Hãy chắc chắn rằng bạn đã đọc các cạm bẫy tuyệt vời của các slide lập trình hướng đối tượng từ một đồng nghiệp Dịch vụ nhà phát triển Sony để hiểu rõ hơn ở đây.) Điều này không "cảm thấy" như tối ưu hóa; đó chủ yếu là thời gian dành cho một công cụ bảng trắng hoặc UML hoặc tạo ra nhiều nguyên mẫu, thay vì làm cho mã hiện tại chạy nhanh hơn. Nhưng nó thường đáng giá hơn nhiều.

Và một heuristic hữu ích khác: nếu bạn ở gần "lõi" của động cơ, có thể cần thêm một số nỗ lực và thử nghiệm để tối ưu hóa (ví dụ: vectơ các bội số ma trận đó!). Càng đi sâu vào cốt lõi, bạn càng ít phải lo lắng về điều đó trừ khi một trong những công cụ định hình của bạn nói với bạn cách khác.


6
  1. Sử dụng đúng cấu trúc dữ liệu và thuật toán lên phía trước.
  2. Đừng tối ưu hóa vi mô cho đến khi bạn lập hồ sơ và biết chính xác điểm nóng của bạn ở đâu.
  3. Đừng lo lắng về việc thông minh. Trình biên dịch đã thực hiện tất cả các thủ thuật nhỏ mà bạn nghĩ đến ("Ồ! Tôi cần nhân bốn số! Tôi sẽ chuyển sang hai!")
  4. Hãy chú ý đến bộ nhớ cache.

1
Dựa vào trình biên dịch chỉ thông minh đến một điểm nhất định. Vâng, nó sẽ thực hiện một số tối ưu hóa lổ nhìn trộm mà bạn không nghĩ tới (và không thể thực hiện nếu không lắp ráp), nhưng không rõ thuật toán của bạn phải làm gì để không thể tối ưu hóa thông minh. Ngoài ra, bạn sẽ ngạc nhiên về số lượng chu kỳ bạn có thể giành được bằng cách triển khai mã quan trọng trong lắp ráp hoặc nội tại .... nếu bạn biết bạn đang làm gì. Trình biên dịch không thông minh như chúng được tạo ra, chúng không biết những việc bạn làm trừ khi bạn nói rõ ràng ở mọi nơi (như sử dụng 'hạn chế' một cách tôn giáo).
Kaj

1
Và một lần nữa tôi phải nhận xét rằng nếu bạn chỉ tìm kiếm các điểm nóng, bạn sẽ bỏ lỡ rất nhiều chu kỳ vì bạn sẽ không tìm thấy bất kỳ chu kỳ bị lừa nào trên bảng (ví dụ: smartpointers .... dereferences ở bất cứ đâu, không bao giờ xuất hiện như một điểm nóng bởi vì hiệu quả toàn bộ chương trình của bạn là một điểm nóng).
Kaj

1
Tôi đồng ý với cả hai điểm của bạn, nhưng tôi sẽ gộp phần lớn trong số đó theo "sử dụng các cấu trúc dữ liệu và thuật toán phù hợp". Nếu bạn đi qua các con trỏ thông minh được đếm lại ở khắp mọi nơi và đang chảy máu theo chu kỳ đếm, bạn chắc chắn đã chọn cấu trúc dữ liệu sai.
khoan hồng

5

Tuy nhiên, hãy nhớ rằng "bi quan sớm". Mặc dù không cần phải khó khăn trong mọi dòng mã, nhưng có lý do để nhận ra bạn đang thực sự làm việc trên một trò chơi, có ý nghĩa về hiệu suất thời gian thực.
Mặc dù mọi người đều bảo bạn đo và tối ưu hóa các điểm nóng, nhưng kỹ thuật đó sẽ không cho bạn thấy hiệu suất bị mất ở những nơi khuất. Ví dụ: nếu mọi thao tác '+' trong mã của bạn sẽ mất gấp đôi thời gian cần thiết, thì nó sẽ không hiển thị như một điểm nóng và do đó bạn sẽ không bao giờ tối ưu hóa nó hoặc thậm chí nhận ra, tuy nhiên vì nó được sử dụng trong quá trình nơi nó có thể chi phí cho bạn rất nhiều hiệu suất. Bạn sẽ ngạc nhiên khi có bao nhiêu chu kỳ nhỏ giọt mà không bị phát hiện. Vì vậy, hãy nhận thức được những gì bạn làm.
Ngoài ra, tôi có xu hướng lập hồ sơ thường xuyên để có ý tưởng về những gì ở đó và thời gian còn lại trên mỗi khung hình. Đối với tôi thời gian trên mỗi khung hình là hợp lý nhất vì nó cho tôi biết trực tiếp nơi tôi đang ở với các mục tiêu khung hình. Ngoài ra, hãy cố gắng tìm ra các đỉnh ở đâu và nguyên nhân gây ra chúng - Tôi thích tốc độ khung hình ổn định hơn tốc độ khung hình cao với gai.


Điều này có vẻ rất sai với tôi. Chắc chắn, '+' của tôi có thể mất gấp đôi thời gian mỗi lần nó được gọi, nhưng điều này thực sự chỉ quan trọng trong một vòng lặp chặt chẽ. Trong một vòng lặp chặt chẽ, việc thay đổi một '+' có thể thực hiện các lệnh có cường độ lớn hơn thay đổi một '+' bên ngoài vòng lặp. Tại sao nghĩ về một phần mười của một phần triệu giây, khi một phần nghìn giây có thể được lưu lại?
Wilduck

1
Sau đó, bạn không hiểu ý tưởng đằng sau mất nhỏ giọt. '+' (giống như một ví dụ) được gọi là hàng trăm nghìn lần trên mỗi khung hình, không chỉ trong các vòng lặp chặt chẽ. Nếu điều đó mất một vài chu kỳ mỗi khi bạn mất rất nhiều chu kỳ trên bảng, nhưng nó sẽ không bao giờ hiển thị dưới dạng điểm nóng do các cuộc gọi được phân bổ đều trên đường dẫn thực hiện / cơ sở mã của bạn. Vì vậy, bạn không nói về một phần mười của một phần triệu giây, nhưng thực sự là hàng ngàn lần những phần mười của phần triệu giây đó, thêm vào nhiều phần nghìn giây. Sau khi đi lấy quả treo thấp (vòng kín) tôi đã đạt được một phần nghìn giây theo cách này hơn một lần.
Kaj

Nó giống như một cái vòi nhỏ giọt. Tại sao phải lo lắng về việc tiết kiệm giọt nhỏ đó? - "Nếu vòi của bạn nhỏ giọt với tốc độ một giọt mỗi giây, bạn có thể sẽ lãng phí 2700 gallon mỗi năm".
Kaj

Ồ, tôi đoán không rõ ý tôi là khi toán tử + bị quá tải, vì vậy nó sẽ ảnh hưởng đến mọi '+' trong mã - bạn thực sự sẽ không muốn tối ưu hóa mọi '+' trong mã. Tôi đoán ví dụ xấu .... Tôi muốn nói nó như một ví dụ cho 'chức năng cốt lõi được gọi ở khắp mọi nơi nơi việc triển khai có thể chậm hơn so với giả định, đặc biệt là khi bị ẩn bởi quá tải toán tử hoặc các cấu trúc C ++ gây khó chịu khác'.
Kaj

3

Khi một trò chơi đã sẵn sàng để được phát hành (bản cuối cùng hoặc bản beta) hoặc chậm một cách đáng chú ý, đó có lẽ là thời điểm tốt nhất để lập hồ sơ cho ứng dụng của bạn. Tất nhiên, bạn luôn có thể chạy trình hồ sơ bất cứ lúc nào; nhưng có, tối ưu hóa sớm là gốc rễ của mọi tội lỗi. Tối ưu hóa vô căn cứ, quá; bạn cần dữ liệu thực tế để chỉ ra rằng một số mã chậm, trước khi bạn nên thử "tối ưu hóa" nó. Một hồ sơ làm điều đó cho bạn.

Nếu bạn không biết về một hồ sơ, hãy tìm hiểu nó! Đây là một bài viết blog tốt cho thấy sự hữu ích của một hồ sơ.

Hầu hết tối ưu hóa mã trò chơi là để giảm các chu kỳ CPU mà bạn cần cho mỗi khung hình. Một cách để làm điều này là chỉ cần tối ưu hóa mọi thói quen khi bạn viết nó và đảm bảo rằng nó càng nhanh càng tốt. Tuy nhiên, có một câu nói phổ biến rằng 90% chu kỳ CPU được sử dụng trong 10% mã. Điều này có nghĩa là việc hướng tất cả công việc tối ưu hóa của bạn vào các thói quen thắt cổ chai này sẽ có hiệu quả gấp 10 lần tối ưu hóa mọi thứ một cách thống nhất. Vậy làm thế nào để bạn xác định những thói quen này? Hồ sơ làm cho nó dễ dàng.

Mặt khác, nếu trò chơi nhỏ của bạn đang chạy ở tốc độ 200 FPS mặc dù nó có thuật toán không hiệu quả trong đó, bạn có thực sự có lý do để tối ưu hóa không? Bạn nên có ý tưởng tốt về thông số kỹ thuật của máy mục tiêu của mình và đảm bảo trò chơi chạy tốt trên máy đó, nhưng bất cứ điều gì ngoài việc đó (có thể nói là lãng phí) thời gian có thể được sử dụng để mã hóa hoặc đánh bóng trò chơi tốt hơn.


Mặc dù quả treo thấp thực sự có xu hướng nằm trong 10% mã, và dễ dàng bị bắt bởi hồ sơ cuối cùng, hoàn toàn làm việc bằng cách định hình cho điều này sẽ khiến bạn bỏ lỡ các thói quen được gọi là rất nhiều nhưng chỉ một chút mỗi đoạn mã xấu - chúng sẽ không hiển thị trong hồ sơ của bạn nhưng chúng đã chảy rất nhiều chu kỳ cho mỗi cuộc gọi. Nó thực sự thêm lên.
Kaj

@Kaj, Trình biên dịch tốt tổng hợp tất cả hàng trăm lần thực hiện riêng lẻ của thuật toán xấu và hiển thị cho bạn tổng số. Tiếp theo bạn sẽ nói "Nhưng nếu bạn có 10 phương pháp xấu và tất cả được gọi với tỷ lệ 1/10 thì sao?" Nếu bạn dành tất cả thời gian cho 10 phương pháp đó, bạn sẽ thiếu tất cả các loại trái cây treo thấp, nơi bạn sẽ nhận được một tiếng nổ lớn hơn nhiều cho đồng tiền của mình.
John McDonald

2

Tôi thấy hữu ích khi xây dựng hồ sơ. Ngay cả khi bạn không chủ động tối ưu hóa, thật tốt khi có ý tưởng về những gì đang hạn chế hiệu suất của bạn tại bất kỳ thời điểm nào. Nhiều trò chơi có một số loại HUD có thể che phủ, hiển thị biểu đồ đồ họa đơn giản (thường chỉ là một thanh màu) cho biết các phần khác nhau của vòng lặp trò chơi sẽ lấy mỗi khung hình trong bao lâu.

Sẽ là một ý tưởng tồi khi để phân tích hiệu suất và tối ưu hóa đến quá muộn một giai đoạn muộn. Nếu bạn đã xây dựng trò chơi và bạn vượt quá 200% ngân sách CPU của mình và bạn không thể tìm thấy điều đó thông qua tối ưu hóa, bạn sẽ bị lừa.

Bạn cần biết ngân sách dành cho đồ họa, vật lý, v.v., khi bạn viết. Bạn không thể làm điều đó nếu bạn không biết hiệu suất của bạn sẽ là gì và bạn không thể đoán được điều đó mà không biết cả hiệu suất của bạn là bao nhiêu và có thể có bao nhiêu sự chậm chạp.

Vì vậy, xây dựng trong một số thống kê hiệu suất từ ​​ngày đầu tiên.

Khi nào nên giải quyết công cụ - một lần nữa, có lẽ tốt nhất không nên để quá muộn, vì sợ bạn phải cấu trúc lại một nửa động cơ của mình. Mặt khác, đừng quá bận tâm vào việc tối ưu hóa mọi thứ để vắt kiệt mọi chu kỳ nếu bạn nghĩ rằng bạn có thể thay đổi thuật toán hoàn toàn vào ngày mai hoặc nếu bạn không đưa dữ liệu trò chơi thực sự vào đó.

Chọn trái cây treo thấp khi bạn đi cùng, giải quyết những thứ lớn theo định kỳ, và bạn sẽ ổn thôi.


Để thêm vào trình tạo hồ sơ ingame (mà tôi hoàn toàn đồng ý), việc mở rộng trình biên dịch ingame của bạn để hiển thị nhiều thanh (cho nhiều khung hình) giúp bạn tương quan hành vi trò chơi với các đột biến và có thể giúp bạn tìm thấy các nút cổ chai không hiển thị trong bản chụp trung bình của bạn với một hồ sơ.
Kaj

2

Nếu nhìn vào trích dẫn của Knuth trong bối cảnh của nó, anh ta tiếp tục giải thích, rằng chúng ta nên tối ưu hóa nhưng với các công cụ, như một hồ sơ.

Bạn nên liên tục hồ sơ và hồ sơ bộ nhớ ứng dụng của bạn sau khi kiến ​​trúc rất cơ bản được đặt.

Hồ sơ sẽ không chỉ giúp bạn tăng tốc độ, nó sẽ giúp bạn tìm ra lỗi. Nếu chương trình của bạn đột nhiên thay đổi mạnh mẽ tốc độ, điều này thường là do lỗi. Nếu bạn không định hình nó có thể không được chú ý.

Bí quyết để tối ưu hóa là làm điều đó bằng thiết kế. Đừng đợi đến phút cuối cùng. Hãy chắc chắn rằng thiết kế chương trình của bạn cung cấp cho bạn hiệu suất bạn cần mà không thực sự có các thủ thuật lặp bên trong khó chịu.


1

Đối với dự án của tôi, tôi thường áp dụng một số tối ưu hóa RẤT cần thiết trong công cụ cơ sở của mình. Ví dụ: tôi luôn muốn triển khai triển khai SIMD vững chắc bằng SSE2 và 3DNow! Điều này đảm bảo toán học dấu phẩy động của tôi nằm trên cue với nơi tôi muốn. Một thực hành tốt khác là tạo thói quen tối ưu hóa khi bạn viết mã thay vì quay lại. Hầu hết thời gian những thực hành nhỏ này chỉ tốn thời gian như những gì bạn đã mã hóa. Trước khi mã hóa một tính năng, hãy đảm bảo bạn nghiên cứu cách hiệu quả nhất để thực hiện.

Tóm lại, theo ý kiến ​​của tôi, HARDER của nó để làm cho mã của bạn hiệu quả hơn sau khi nó đã bị hút.


0

Tôi muốn nói rằng cách dễ nhất sẽ là sử dụng ý thức chung của bạn - nếu một cái gì đó có vẻ như nó chạy chậm, thì hãy xem nó. Xem nếu nó là một nút cổ chai.
Sử dụng một hồ sơ để xem các chức năng tốc độ đang sử dụng và tần suất chúng được gọi.
Hoàn toàn không có điểm tối ưu hóa hoặc dành thời gian cố gắng tối ưu hóa một cái gì đó không cần nó.


0

Nếu mã của bạn đang chạy chậm, thì hãy chạy một trình lược tả và xem chính xác điều gì đang khiến nó chạy chậm hơn. Hoặc bạn có thể là người chủ động và đã có một trình hồ sơ chạy trước đó bạn bắt đầu nhận thấy các vấn đề về hiệu suất.

Bạn sẽ muốn tối ưu hóa khi tốc độ khung hình của bạn giảm xuống đến mức mà trò chơi bắt đầu phải chịu đựng. Thủ phạm rất có thể của bạn sẽ là CPU của bạn bị sử dụng quá nhiều (100%).


Tôi muốn nói GPU cũng giống như CPU. Thật vậy, tùy thuộc vào mức độ kết hợp chặt chẽ của mọi thứ, hoàn toàn có khả năng CPU bị ràng buộc rất nhiều trong một nửa khung hình và GPU bị ràng buộc nặng nề với nửa kia. Hồ sơ ngu ngốc thậm chí có thể hiển thị cách sử dụng ít hơn 100% trên một trong hai. Hãy chắc chắn rằng hồ sơ của bạn đủ mịn để thể hiện điều đó (nhưng không quá nhỏ đến mức bị xâm phạm!)
JasonD

0

Bạn nên tối ưu hóa mã của mình ... thường xuyên như bạn cần.

Những gì tôi đã làm trong quá khứ chỉ là liên tục chạy trò chơi với bật hồ sơ (ít nhất là một bộ đếm tốc độ khung hình trên màn hình mọi lúc). Nếu trò chơi đang trở nên chậm (chẳng hạn như tốc độ khung hình mục tiêu của bạn trên máy spec spec của bạn), hãy bật trình hồ sơ và xem có điểm nóng nào xuất hiện không.

Đôi khi nó không phải là mã. Rất nhiều vấn đề tôi gặp phải trong quá khứ đã được định hướng theo gpu (được cho là, điều này là trên iPhone). Điền vào các vấn đề, quá nhiều cuộc gọi vẽ, không đủ khối hình học, trình đổ bóng không hiệu quả ...

Khác với các thuật toán không hiệu quả cho các vấn đề khó khăn (ví dụ như tìm đường, vật lý), tôi rất hiếm khi gặp phải các vấn đề trong đó chính mã là thủ phạm. Và những vấn đề khó khăn đó phải là những thứ bạn dành rất nhiều nỗ lực để có được thuật toán đúng và không phải lo lắng về những điều nhỏ hơn.


0

Đối với tôi là tốt nhất theo mô hình dữ liệu chuẩn bị tốt. Và tối ưu hóa - trước khi bước chính về phía trước. Tôi có nghĩa là trước khi bắt đầu thực hiện một cái gì đó lớn mới. Lý do khác để tối ưu hóa là khi tôi mất quyền kiểm soát tài nguyên, Ứng dụng cần rất nhiều tải CPU / GPU hoặc bộ nhớ và tôi không biết tại sao :) hoặc nó quá nhiều.

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.