Tôi nên có bao nhiêu chủ đề, và để làm gì?


81

Tôi có nên có các luồng riêng biệt để kết xuất và logic, hoặc thậm chí nhiều hơn không?

Tôi nhận thấy sự sụt giảm hiệu năng to lớn do đồng bộ hóa dữ liệu gây ra (huống chi là bất kỳ khóa mutex nào).

Tôi đã nghĩ đến việc đưa nó đến mức cực đoan và thực hiện các chủ đề để có thể hiểu được mọi hệ thống con có thể hiểu được. Nhưng tôi lo lắng rằng có thể làm mọi thứ chậm lại. (Ví dụ: việc tách luồng đầu vào khỏi kết xuất hoặc luồng logic trò chơi có lành mạnh không?) Việc đồng bộ hóa dữ liệu được yêu cầu có làm cho nó trở nên vô nghĩa hoặc thậm chí chậm hơn không?


6
nền tảng nào? PC, bảng điều khiển NextGen, điện thoại thông minh?
Ellis

Có một điều mà tôi có thể nghĩ rằng sẽ yêu cầu đa luồng; mạng.
Xà phòng

thoát khỏi sự háo hức, không có sự chậm chạp "mênh mông" khi có khóa. Đây là một truyền thuyết đô thị, và một định kiến.
v.oddou

Câu trả lời:


61

Cách tiếp cận phổ biến để tận dụng nhiều lõi là, thẳng thắn, chỉ đơn giản là sai lầm. Việc tách các hệ thống con của bạn thành các luồng khác nhau thực sự sẽ phân tách một số công việc trên nhiều lõi, nhưng nó có một số vấn đề lớn. Đầu tiên, rất khó để làm việc với. Ai muốn làm quen với các khóa và đồng bộ hóa và giao tiếp và các công cụ khi họ chỉ có thể viết thẳng lên kết xuất hoặc mã vật lý? Thứ hai, cách tiếp cận không thực sự mở rộng quy mô. Tốt nhất, điều này sẽ cho phép bạn tận dụng khoảng ba hoặc bốn lõi, và đó là nếu bạn thực sự biết bạn đang làm gì. Chỉ có rất nhiều hệ thống con trong một trò chơi, và trong số chúng thậm chí còn ít hơn chiếm nhiều thời gian CPU. Có một vài lựa chọn thay thế tốt mà tôi biết.

Một là có một luồng chính cùng với luồng công nhân cho mỗi CPU bổ sung. Bất kể hệ thống con, luồng chính ủy nhiệm các nhiệm vụ riêng biệt cho luồng xử lý công nhân thông qua một số loại hàng đợi; những nhiệm vụ này cũng có thể tự tạo ra các nhiệm vụ khác. Mục đích duy nhất của các luồng công nhân là mỗi nhiệm vụ lấy từ hàng đợi một lần và thực hiện chúng. Tuy nhiên, điều quan trọng nhất là ngay khi một luồng cần kết quả của một nhiệm vụ, nếu nhiệm vụ được hoàn thành, nó có thể nhận được kết quả và nếu không, nó có thể loại bỏ nhiệm vụ khỏi hàng đợi một cách an toàn và tiếp tục thực hiện điều đó nhiệm vụ chính nó. Đó là, không phải tất cả các nhiệm vụ cuối cùng sẽ được lên lịch song song với nhau. Có nhiều nhiệm vụ hơn có thể được thực hiện song song là tốtđiều trong trường hợp này; nó có nghĩa là nó có khả năng mở rộng khi bạn thêm nhiều lõi. Một nhược điểm của điều này là nó đòi hỏi rất nhiều công việc trước mắt để thiết kế một hàng đợi và vòng lặp công nhân đàng hoàng trừ khi bạn có quyền truy cập vào thư viện hoặc thời gian chạy ngôn ngữ đã cung cấp điều này cho bạn. Phần khó nhất là đảm bảo các nhiệm vụ của bạn thực sự tách biệt và an toàn cho luồng, và đảm bảo các nhiệm vụ của bạn ở trong một khu vực giữa hạnh phúc giữa hạt thô và hạt mịn.

Một cách khác để xử lý các luồng hệ thống con là song song hóa từng hệ thống con trong sự cô lập. Đó là, thay vì chạy kết xuất và vật lý trong các luồng của riêng họ, hãy viết hệ thống con vật lý để sử dụng tất cả các lõi của bạn cùng một lúc, hãy viết hệ thống con kết xuất để sử dụng tất cả các lõi của bạn cùng một lúc, sau đó hai hệ thống chỉ cần chạy tuần tự (hoặc xen kẽ, tùy thuộc vào các khía cạnh khác của kiến ​​trúc trò chơi của bạn). Ví dụ, trong hệ thống con vật lý, bạn có thể lấy tất cả khối lượng điểm trong trò chơi, chia chúng ra giữa các lõi của bạn và sau đó có tất cả các lõi cập nhật chúng cùng một lúc. Mỗi lõi sau đó có thể làm việc trên dữ liệu của bạn trong các vòng lặp chặt chẽ với địa phương tốt. Kiểu song song bước khóa này tương tự như những gì GPU làm. Phần khó nhất ở đây là đảm bảo rằng bạn đang phân chia công việc của mình thành các phần nhỏ mịn để phân chia công bằngthực sự dẫn đến một lượng công việc như nhau trên tất cả các bộ xử lý.

Tuy nhiên, đôi khi nó chỉ đơn giản nhất, do chính trị, mã hiện có hoặc các tình huống bực bội khác, để cung cấp cho mỗi hệ thống con một luồng. Trong trường hợp đó, tốt nhất là tránh tạo ra nhiều luồng hệ điều hành hơn lõi cho khối lượng công việc nặng của CPU (nếu bạn có thời gian chạy với các luồng nhẹ chỉ xảy ra để cân bằng giữa các lõi của bạn, thì đây không phải là vấn đề lớn). Ngoài ra, tránh giao tiếp quá mức. Một mẹo hay là thử dùng pipelining; mỗi hệ thống con chính có thể hoạt động trên một trạng thái trò chơi khác nhau tại một thời điểm. Đường ống làm giảm lượng giao tiếp cần thiết giữa các hệ thống con của bạn vì chúng không cần truy cập vào cùng một dữ liệu cùng một lúc và nó cũng có thể vô hiệu hóa một số thiệt hại do tắc nghẽn. Ví dụ, nếu hệ thống con vật lý của bạn có xu hướng mất nhiều thời gian để hoàn thành và hệ thống con kết xuất của bạn luôn chờ đợi nó, tốc độ khung hình tuyệt đối của bạn có thể cao hơn nếu bạn chạy hệ thống con vật lý cho khung hình tiếp theo trong khi hệ thống con kết xuất vẫn hoạt động trên trước đó khung. Trong thực tế, nếu bạn có các tắc nghẽn như vậy và không thể loại bỏ chúng theo bất kỳ cách nào khác, đường ống có thể là lý do chính đáng nhất để bận tâm với các luồng hệ thống con.


"Ngay khi một tiểu trình cần kết quả của một nhiệm vụ, nếu nhiệm vụ được hoàn thành, nó có thể nhận được kết quả và nếu không, nó có thể loại bỏ nhiệm vụ khỏi hàng đợi một cách an toàn và tiếp tục thực hiện nhiệm vụ đó". Bạn đang nói về một nhiệm vụ được sinh ra bởi cùng một chủ đề? Nếu vậy, thì nó sẽ có ý nghĩa hơn nếu tác vụ đó được thực thi bởi luồng tự sinh ra nhiệm vụ?
jmp97

tức là luồng có thể, mà không lập lịch tác vụ, thực thi tác vụ đó ngay lập tức.
jmp97

3
Vấn đề là luồng không nhất thiết phải biết trước liệu có nên chạy song song nhiệm vụ hay không. Ý tưởng là để châm ngòi cho công việc mà cuối cùng bạn sẽ cần thực hiện, và nếu một chủ đề khác thấy nó không hoạt động thì nó có thể tiếp tục và thực hiện công việc này cho bạn. Nếu điều này kết thúc không xảy ra vào thời điểm bạn cần kết quả, bạn có thể tự mình thực hiện nhiệm vụ từ hàng đợi. Sơ đồ này là để tự động cân bằng một khối lượng công việc trên nhiều lõi thay vì tĩnh.
Jake McArthur

Xin lỗi vì mất quá nhiều thời gian để trở lại chủ đề này. Gần đây tôi không chú ý đến gamedev. Đây có lẽ là câu trả lời tốt nhất, thẳng thừng nhưng đến mức và rộng rãi.
j đối thủ

1
Bạn đúng theo nghĩa mà tôi đã bỏ qua khi nói về khối lượng công việc nặng I / O. Giải thích của tôi về câu hỏi là chỉ về khối lượng công việc nặng CPU.
Jake McArthur

30

Có một vài điều cần xem xét. Lộ trình luồng trên mỗi hệ thống con rất dễ nghĩ đến vì việc phân tách mã khá rõ ràng từ việc di chuyển. Tuy nhiên, tùy thuộc vào mức độ liên lạc của các hệ thống con của bạn, giao tiếp giữa các luồng có thể thực sự giết chết hiệu suất của bạn. Ngoài ra, điều này chỉ chia tỷ lệ thành N lõi, trong đó N là số lượng hệ thống con bạn trừu tượng thành các luồng.

Nếu bạn chỉ muốn đa nhiệm một trò chơi hiện có, đây có lẽ là con đường ít kháng cự nhất. Tuy nhiên, nếu bạn đang làm việc trên một số hệ thống động cơ cấp thấp có thể được chia sẻ giữa một số trò chơi hoặc dự án, tôi sẽ xem xét một phương pháp khác.

Nó có thể mất một chút tâm trí xoắn, nhưng nếu bạn có thể phá vỡ mọi thứ như một hàng đợi công việc với một chuỗi các luồng công nhân, nó sẽ mở rộng tốt hơn nhiều trong thời gian dài. Khi các chip mới nhất và lớn nhất xuất hiện với số lõi đáng kinh ngạc, hiệu suất trò chơi của bạn sẽ mở rộng cùng với nó, chỉ cần kích hoạt thêm các luồng công nhân.

Về cơ bản, nếu bạn đang tìm kiếm một số dự án song song cho một dự án hiện có, tôi sẽ song song hóa các hệ thống con. Nếu bạn đang xây dựng một công cụ mới từ đầu với khả năng mở rộng song song, tôi sẽ xem xét một hàng đợi công việc.


Hệ thống mà bạn đề cập rất giống với hệ thống lập lịch được đề cập trong câu trả lời do Other James đưa ra, vẫn có chi tiết tốt trong lĩnh vực đó nên +1 vì nó thêm vào cuộc thảo luận.
James

3
một wiki cộng đồng về cách thiết lập hàng đợi công việc và các luồng công nhân sẽ rất tốt.
bot_bot

23

Câu hỏi đó không có câu trả lời tốt nhất, vì nó phụ thuộc vào những gì bạn đang cố gắng thực hiện.

Xbox có ba lõi và có thể xử lý một vài luồng trước khi chuyển đổi ngữ cảnh trở thành vấn đề. Các pc có thể đối phó với khá nhiều hơn nữa.

Rất nhiều trò chơi thường được phân luồng đơn để dễ lập trình. Điều này là tốt cho hầu hết các trò chơi cá nhân. Điều duy nhất bạn có thể sẽ phải có một chủ đề khác là Mạng và Âm thanh.

Unreal có một luồng trò chơi, kết xuất luồng, luồng mạng và luồng âm thanh (nếu tôi nhớ chính xác). Đây là tiêu chuẩn khá cho nhiều động cơ thế hệ hiện tại, mặc dù việc có thể hỗ trợ một luồng kết xuất riêng biệt có thể là một nỗi đau và liên quan đến rất nhiều nền tảng.

Công cụ idTech5 đang được phát triển cho Rage thực sự sử dụng bất kỳ số lượng luồng nào và nó cũng làm như vậy bằng cách chia nhỏ các tác vụ trò chơi thành 'công việc' được xử lý bằng hệ thống tác vụ. Mục tiêu rõ ràng của họ là có quy mô công cụ trò chơi độc đáo khi số lượng lõi trên hệ thống chơi game trung bình tăng vọt.

Công nghệ tôi sử dụng (và đã viết) có một luồng riêng biệt cho Mạng, Đầu vào, Âm thanh, Kết xuất và Lập lịch. Sau đó, nó có bất kỳ số lượng luồng nào có thể được sử dụng để thực hiện các tác vụ trò chơi và điều này được quản lý bởi luồng lập lịch. Rất nhiều công việc đã đi vào để có được tất cả các luồng để chơi tốt với nhau, nhưng có vẻ như nó hoạt động tốt và sử dụng rất tốt các hệ thống đa lõi, vì vậy có lẽ đó là nhiệm vụ đã hoàn thành (hiện tại; tôi có thể phá vỡ âm thanh / mạng / nhập công việc vào chỉ 'nhiệm vụ' mà các luồng công nhân có thể cập nhật).

Nó thực sự phụ thuộc vào mục tiêu cuối cùng của bạn.


+1 cho việc đề cập đến một hệ thống Lập kế hoạch .. thường là một nơi tốt để tập trung vào giao tiếp chủ đề / hệ thống :)
James

Tại sao bỏ phiếu xuống, downvoter?
jcora

12

Một luồng trên mỗi hệ thống con là cách sai. Đột nhiên, ứng dụng của bạn sẽ không mở rộng được vì một số hệ thống con đòi hỏi nhiều hơn những hệ thống khác. Đây là cách tiếp cận luồng được thực hiện bởi Chỉ huy tối cao và nó không vượt quá hai lõi vì chúng chỉ có hai hệ thống con chiếm một lượng đáng kể kết xuất CPU và logic vật lý / trò chơi, mặc dù chúng có 16 luồng, các luồng khác chỉ vừa đủ với bất kỳ công việc nào và kết quả là trò chơi chỉ thu được hai nhân.

Những gì bạn nên làm là sử dụng một cái gì đó gọi là một nhóm chủ đề. Điều này phần nào phản ánh cách tiếp cận được thực hiện trên GPU - nghĩa là, bạn đăng công việc và bất kỳ chủ đề có sẵn nào chỉ đơn giản xuất hiện và thực hiện nó, sau đó quay lại chờ công việc - nghĩ về nó như bộ đệm vòng, của các luồng. Cách tiếp cận này có ưu điểm là nhân rộng tỷ lệ N-core và rất tốt trong việc nhân rộng cho cả số lượng lõi thấp và cao. Nhược điểm là khá khó để thực hiện quyền sở hữu luồng cho phương pháp này, vì không thể biết luồng nào đang thực hiện công việc tại bất kỳ thời điểm nào, do đó bạn phải khóa các vấn đề quyền sở hữu rất chặt chẽ. Nó cũng làm cho việc sử dụng các công nghệ như Direct3D9 không hỗ trợ nhiều luồng.

Nhóm luồng rất khó sử dụng, nhưng chúng mang lại kết quả tốt nhất có thể. Nếu bạn cần mở rộng quy mô cực kỳ tốt, hoặc bạn có nhiều thời gian để làm việc với nó, hãy sử dụng một nhóm luồng. Nếu bạn đang cố gắng đưa song song vào một dự án hiện có với các vấn đề phụ thuộc không xác định và các công nghệ đơn luồng, thì đây không phải là giải pháp cho bạn.


Nói chính xác hơn một chút: GPU không sử dụng nhóm luồng thay vào đó bộ lập lịch luồng được triển khai trong phần cứng, điều này khiến cho việc tạo luồng và chuyển đổi luồng mới rất tốn kém, trái ngược với CPU, nơi việc tạo luồng và chuyển đổi ngữ cảnh rất tốn kém. Xem Hướng dẫn lập trình viên Nvidias CUDA chẳng hạn.
Nils

2
+1: Câu trả lời hay nhất tại đây. Tôi thậm chí sẽ sử dụng các cấu trúc trừu tượng hơn các luồng (ví dụ hàng đợi công việc và công nhân) nếu khung của bạn cho phép nó. Việc nghĩ / lập trình theo thuật ngữ này dễ hơn nhiều so với trong các chủ đề / khóa / thuần túy. Plus: Chia nhỏ trò chơi của bạn trong kết xuất, logic, v.v. là vô nghĩa, vì việc kết xuất phải chờ logic kết thúc. Thay vì tạo các công việc thực sự có thể được thực thi song song (ví dụ: Tính toán AI cho một npc cho khung tiếp theo).
Dave O.

@DaveO. Điểm "Plus" của bạn là như vậy, rất đúng.
Kỹ sư

11

Bạn đúng rằng phần quan trọng nhất là tránh đồng bộ hóa bất cứ nơi nào có thể. Có một vài cách để đạt được điều này.

  1. Biết dữ liệu của bạn và lưu trữ nó trong bộ nhớ theo nhu cầu xử lý của bạn. Điều này cho phép bạn lập kế hoạch cho các tính toán song song mà không cần đồng bộ hóa. Đáng tiếc đây là phần lớn thời gian khá khó để đạt được vì dữ liệu thường được truy cập từ các hệ thống khác nhau vào những thời điểm không thể đoán trước.

  2. Xác định thời gian truy cập rõ ràng cho dữ liệu. Bạn có thể tách dấu tick chính của mình thành các pha x. Nếu bạn chắc chắn rằng Thread X chỉ đọc dữ liệu trong một pha cụ thể, bạn cũng biết rằng dữ liệu này có thể được sửa đổi bởi các luồng khác trong một pha khác.

  3. Nhân đôi dữ liệu của bạn. Đó là cách tiếp cận đơn giản nhất, nhưng nó làm tăng độ trễ, vì Thread X đang làm việc với dữ liệu từ khung cuối cùng, trong khi Thread Y đang chuẩn bị dữ liệu cho khung tiếp theo.

Kinh nghiệm cá nhân của tôi cho thấy các tính toán hạt mịn là cách hiệu quả nhất, vì chúng có thể mở rộng tốt hơn nhiều so với các giải pháp dựa trên hệ thống con. Nếu bạn xâu chuỗi các hệ thống con của mình, thời gian khung sẽ bị ràng buộc với hệ thống con đắt nhất. Điều này có thể dẫn đến tất cả các luồng nhưng chỉ một lần chạy cho đến khi hệ thống con đắt tiền cuối cùng đã hoàn thành. Nếu bạn có thể tách các phần lớn của trò chơi thành các nhiệm vụ nhỏ, các tác vụ này có thể được lên lịch phù hợp để tránh các lõi không hoạt động. Nhưng đây là điều khó thực hiện nếu bạn đã có một cơ sở mã lớn.

Để xem xét một số hạn chế về phần cứng, bạn nên cố gắng không bao giờ đăng ký quá mức phần cứng của mình. Với đăng ký vượt mức, tôi có nghĩa là có nhiều luồng phần mềm hơn các luồng phần cứng nền tảng của bạn. Đặc biệt trên các kiến ​​trúc PPC (Xbox360, PS3), một chuyển đổi tác vụ thực sự tốn kém. Tất nhiên là hoàn toàn ổn nếu bạn có một số luồng được đăng ký vượt mức chỉ được kích hoạt trong một khoảng thời gian nhỏ (ví dụ như một khung hình) Nếu bạn nhắm mục tiêu vào PC, bạn nên nhớ rằng số lượng lõi (hoặc CTNH tốt hơn -Threads) không ngừng phát triển, vì vậy bạn sẽ muốn tìm một giải pháp có thể mở rộng, tận dụng lợi thế của CPU-Power bổ sung. Vì vậy, trong lĩnh vực này, bạn nên cố gắng thiết kế mã của mình dựa trên nhiệm vụ càng tốt.


3

Nguyên tắc chung cho việc xâu chuỗi một ứng dụng: 1 luồng trên mỗi CPU CPU. Trên PC lõi tứ có nghĩa là 4. Như đã lưu ý, XBox 360 tuy nhiên có 3 lõi nhưng mỗi luồng có 2 phần cứng, do đó có 6 luồng trong trường hợp này. Trên một hệ thống như PS3 ... chúc may mắn trên đó :) Mọi người vẫn đang cố gắng tìm ra nó.

Tôi sẽ đề nghị thiết kế mỗi hệ thống như một mô-đun khép kín mà bạn có thể xâu chuỗi nếu muốn. Điều này thường có nghĩa là có các đường truyền thông được xác định rất rõ ràng giữa mô-đun và phần còn lại của động cơ. Tôi đặc biệt thích các quy trình Chỉ đọc như Kết xuất và âm thanh cũng như 'chúng ta đã ở đó chưa' các quy trình như đọc đầu vào của trình phát cho mọi thứ được xử lý. Để chạm vào câu trả lời được đưa ra bởi AttackHobo, khi bạn hiển thị 30-60fps, nếu dữ liệu của bạn là 1/30/1/60 giây, nó thực sự sẽ không làm giảm cảm giác phản hồi của trò chơi của bạn. Luôn nhớ rằng sự khác biệt chính giữa phần mềm ứng dụng và trò chơi video là làm mọi thứ 30-60 lần một giây. Tuy nhiên, trên cùng một lưu ý,

Nếu bạn thiết kế hệ thống động cơ của mình đủ tốt, bất kỳ hệ thống nào trong số chúng có thể được chuyển từ luồng này sang luồng khác để cân bằng động cơ của bạn một cách phù hợp hơn trên cơ sở mỗi trò chơi và tương tự. Về lý thuyết, bạn cũng có thể sử dụng công cụ của mình trong một hệ thống phân tán nếu cần là nơi các hệ thống máy tính hoàn toàn riêng biệt chạy từng thành phần.


2
Xbox360 có 2 ổ cứng cho mỗi lõi, vì vậy số lượng luồng tối ưu là 6.
DarthCoder

À, +1 :) Tôi luôn bị giới hạn trong các khu vực kết nối của 360 và ps3, hehe :)
James

0

Tôi tạo một luồng trên mỗi lõi logic (trừ một luồng, để tính đến Main Thread, người tình cờ chịu trách nhiệm kết xuất, nhưng mặt khác cũng hoạt động như một luồng công nhân).

Tôi thu thập các sự kiện thiết bị đầu vào trong thời gian thực trong suốt một khung, nhưng không áp dụng chúng cho đến hết khung: chúng sẽ có hiệu lực trong khung tiếp theo. Và tôi sử dụng một logic tương tự để kết xuất (trạng thái cũ) so với cập nhật (trạng thái mới).

Tôi sử dụng các sự kiện nguyên tử để trì hoãn các hoạt động không an toàn cho đến sau này trong cùng một khung và tôi sử dụng nhiều hơn một hàng đợi sự kiện (hàng đợi công việc) để thực hiện một rào cản bộ nhớ mang lại sự đảm bảo chắc chắn về trật tự hoạt động, mà không bị khóa hoặc chờ đợi (khóa hàng đợi đồng thời miễn phí theo thứ tự ưu tiên công việc).

Đáng chú ý là phải đề cập rằng bất kỳ công việc nào cũng có thể phát ra các subjobs (tốt hơn và tiếp cận nguyên tử) cho cùng một hàng đợi ưu tiên hoặc một công việc cao hơn (được phục vụ sau trong khung).

Do tôi có ba hàng đợi như vậy, tất cả các luồng trừ một luồng có khả năng bị đình trệ chính xác ba lần trên mỗi khung (trong khi chờ các luồng khác hoàn thành tất cả các công việc còn tồn tại được cấp ở mức ưu tiên hiện tại).

Đây có vẻ là một mức độ không chấp nhận được của chủ đề không hoạt động!


Khung của tôi bắt đầu bằng MAIN hiển thị OLD STATE từ lượt cập nhật của khung trước đó, trong khi tất cả các luồng khác ngay lập tức bắt đầu tính toán trạng thái khung NEXT, tôi chỉ sử dụng Sự kiện để nhân đôi thay đổi trạng thái bộ đệm cho đến khi một điểm trong khung không còn ai đọc nữa .
Homer

0

Tôi thường sử dụng một luồng chính (rõ ràng) và tôi sẽ thêm một luồng mỗi khi tôi nhận thấy hiệu suất giảm khoảng 10 đến 20 phần trăm. Để khắc phục sự sụt giảm như vậy, tôi sử dụng các công cụ hiệu suất của studio trực quan. Các sự kiện phổ biến là (un) tải một số khu vực của bản đồ hoặc thực hiện một số tính toán nặ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.