Phân tách hiệu quả các bước Đọc / Tính toán / Viết để xử lý đồng thời các thực thể trong các hệ thống Thực thể / Thành phần


11

Thiết lập

Tôi có một kiến ​​trúc thành phần thực thể trong đó Thực thể có thể có một tập các thuộc tính (là dữ liệu thuần túy không có hành vi) và tồn tại các hệ thống chạy logic thực thể hoạt động trên dữ liệu đó. Về cơ bản, trong mã giả phần nào:

Entity
{
    id;
    map<id_type, Attribute> attributes;
}

System
{
    update();
    vector<Entity> entities;
}

Một hệ thống chỉ di chuyển dọc theo tất cả các thực thể với tốc độ không đổi có thể là

MovementSystem extends System
{
   update()
   {
      for each entity in entities
        position = entity.attributes["position"];
        position += vec3(1,1,1);
   }
}

Về cơ bản, tôi đang cố gắng song song cập nhật () hiệu quả nhất có thể. Điều này có thể được thực hiện bằng cách chạy song song toàn bộ hệ thống hoặc bằng cách cung cấp cho mỗi bản cập nhật () của một hệ thống một vài thành phần để các luồng khác nhau có thể thực hiện cập nhật của cùng một hệ thống, nhưng đối với một tập hợp con khác của các thực thể được đăng ký với hệ thống đó.

Vấn đề

Trong trường hợp của MovementSystem được hiển thị, việc song song hóa là không đáng kể. Vì các thực thể không phụ thuộc vào nhau và không sửa đổi dữ liệu được chia sẻ, chúng tôi có thể di chuyển song song tất cả các thực thể.

Tuy nhiên, các hệ thống này đôi khi yêu cầu các thực thể tương tác với (đọc / ghi dữ liệu từ / đến) lẫn nhau, đôi khi trong cùng một hệ thống, nhưng thường là giữa các hệ thống khác nhau phụ thuộc vào nhau.

Ví dụ, trong một hệ thống vật lý đôi khi các thực thể có thể tương tác với nhau. Hai đối tượng va chạm, vị trí, vận tốc của chúng và các thuộc tính khác được đọc từ chúng, được cập nhật và sau đó các thuộc tính được cập nhật được ghi lại cho cả hai thực thể.

Và trước khi hệ thống kết xuất trong công cụ có thể bắt đầu kết xuất các thực thể, nó phải chờ các hệ thống khác hoàn thành thực thi để đảm bảo rằng tất cả các thuộc tính có liên quan là những gì chúng cần phải có.

Nếu chúng ta cố gắng song song một cách mù quáng điều này, nó sẽ dẫn đến các điều kiện chủng tộc cổ điển nơi các hệ thống khác nhau có thể đọc và sửa đổi dữ liệu cùng một lúc.

Lý tưởng nhất là tồn tại một giải pháp trong đó tất cả các hệ thống có thể đọc dữ liệu từ bất kỳ thực thể nào mà nó muốn, mà không phải lo lắng về các hệ thống khác sửa đổi cùng một dữ liệu đó và không cần lập trình viên quan tâm đến việc sắp xếp đúng cách thực hiện và song song hóa các hệ thống này bằng tay (đôi khi có thể thậm chí không thể).

Trong một triển khai cơ bản, điều này có thể đạt được bằng cách chỉ cần đặt tất cả các dữ liệu đọc và ghi vào các phần quan trọng (bảo vệ chúng bằng các mutexes). Nhưng điều này gây ra một lượng lớn chi phí thời gian chạy và có lẽ không phù hợp với các ứng dụng nhạy cảm hiệu năng.

Giải pháp?

Theo tôi, một giải pháp khả thi sẽ là một hệ thống phân tách đọc / cập nhật và ghi dữ liệu, do đó, trong một giai đoạn đắt tiền, các hệ thống chỉ đọc dữ liệu và tính toán những gì chúng cần để tính toán, bằng cách nào đó lưu trữ kết quả và sau đó viết tất cả dữ liệu đã thay đổi trở lại các thực thể đích trong một lần viết riêng. Tất cả các hệ thống sẽ hành động trên dữ liệu ở trạng thái ở đầu khung và sau đó trước khi kết thúc khung, khi tất cả các hệ thống đã cập nhật xong, một lần viết nối tiếp xảy ra trong đó các kết quả được lưu trong bộ nhớ cache từ tất cả các kết quả khác nhau các hệ thống được lặp lại thông qua và được viết lại cho các thực thể đích.

Điều này dựa trên ý tưởng (có thể sai?) Rằng chiến thắng song song dễ dàng có thể đủ lớn để vượt chi phí (cả về hiệu năng thời gian chạy cũng như chi phí mã) của bộ đệm kết quả và vượt qua ghi.

Câu hỏi

Làm thế nào một hệ thống như vậy có thể được thực hiện để đạt được hiệu suất tối ưu? Các chi tiết triển khai của một hệ thống như vậy là gì và các điều kiện tiên quyết cho một hệ thống Thực thể-Thành phần muốn sử dụng giải pháp này là gì?

Câu trả lời:


1

----- (dựa trên câu hỏi sửa đổi)

Điểm đầu tiên: vì bạn không đề cập đến việc cấu hình thời gian chạy bản phát hành và tìm thấy một nhu cầu cụ thể, tôi khuyên bạn nên làm điều đó càng sớm càng tốt. Hồ sơ của bạn trông như thế nào, bạn đang đập bộ nhớ cache với bố cục bộ nhớ kém, là một lõi được chốt ở mức 100%, thời gian tương đối được dành để xử lý ECS của bạn so với phần còn lại của động cơ, v.v ...

Đọc từ một thực thể và tính toán một cái gì đó ... và giữ kết quả ở đâu đó trong một khu vực lưu trữ trung gian cho đến sau này? Tôi không nghĩ rằng bạn có thể tách biệt đọc + tính toán + lưu trữ theo cách bạn nghĩ và mong đợi cửa hàng trung gian này là bất cứ thứ gì ngoài chi phí thuần túy.

Ngoài ra, vì bạn đang xử lý liên tục, quy tắc chính bạn muốn tuân theo là có một luồng trên mỗi lõi CPU. Tôi nghĩ rằng bạn đang xem xét điều này ở lớp sai , hãy thử nhìn vào toàn bộ hệ thống chứ không phải các thực thể riêng lẻ.

Tạo một biểu đồ phụ thuộc giữa các hệ thống của bạn, một cây gồm những gì hệ thống cần kết quả từ công việc của hệ thống trước đó. Khi bạn có cây phụ thuộc đó rồi, bạn có thể dễ dàng gửi toàn bộ hệ thống đầy đủ các thực thể để xử lý trên một luồng.

Vì vậy, hãy nói rằng cây phụ thuộc của bạn là mớ hỗn độn và bẫy gấu, một vấn đề thiết kế nhưng chúng ta phải làm việc với những gì chúng ta có. Trường hợp tốt nhất ở đây là bên trong mỗi hệ thống, mỗi thực thể không phụ thuộc vào bất kỳ kết quả nào khác bên trong hệ thống đó. Tại đây, bạn dễ dàng phân chia quá trình xử lý trên các luồng, 0-99 và 100-199 trên hai luồng cho một ví dụ với hai lõi và 200 thực thể mà hệ thống này sở hữu.

Trong cả hai trường hợp, ở mỗi giai đoạn bạn phải chờ kết quả mà giai đoạn tiếp theo phụ thuộc vào. Nhưng điều này là ổn vì chờ kết quả của mười khối dữ liệu lớn được xử lý hàng loạt vượt trội hơn nhiều so với việc đồng bộ hóa hàng ngàn lần cho các khối nhỏ.

Ý tưởng đằng sau việc xây dựng một biểu đồ phụ thuộc là tầm thường hóa nhiệm vụ dường như bất khả thi là "Tìm kiếm và lắp ráp các hệ thống khác để chạy song song" bằng cách tự động hóa nó. Nếu một biểu đồ như vậy có dấu hiệu bị chặn bởi liên tục chờ kết quả trước đó thì việc tạo đọc + sửa đổi và ghi chậm chỉ di chuyển chặn và không loại bỏ tính chất nối tiếp của quá trình xử lý.

Và xử lý nối tiếp chỉ có thể được quay song song giữa mỗi điểm chuỗi, nhưng không thể tổng thể. Nhưng bạn nhận ra điều này bởi vì nó là cốt lõi của vấn đề của bạn. Ngay cả khi bộ đệm của bạn đọc từ dữ liệu chưa được ghi nhưng bạn vẫn cần đợi bộ đệm đó sẵn sàng.

Nếu việc tạo ra các kiến ​​trúc song song là dễ dàng hoặc thậm chí có thể với các loại ràng buộc này thì khoa học máy tính sẽ không phải vật lộn với vấn đề kể từ Bletchley Park.

Giải pháp thực sự duy nhất là giảm thiểu tất cả các phụ thuộc này để làm cho các điểm trình tự hiếm khi cần thiết nhất có thể. Điều này có thể liên quan đến việc phân chia các hệ thống thành các bước xử lý tuần tự trong đó, bên trong mỗi hệ thống con, đi song song với các luồng trở nên tầm thường.

Tốt nhất tôi đã giải quyết vấn đề này và thực sự không có gì khác hơn là khuyên rằng nếu đập đầu vào tường gạch đau thì hãy phá vỡ nó thành những bức tường gạch nhỏ hơn để bạn chỉ đánh vào cẳng chân của mình.


Tôi rất tiếc phải nói với bạn, nhưng câu trả lời này có vẻ không hiệu quả. Bạn đang nói với tôi rằng những gì tôi đang tìm kiếm không tồn tại, điều này có vẻ sai về mặt logic (ít nhất là về nguyên tắc) và cũng bởi vì tôi đã thấy mọi người ám chỉ một hệ thống như vậy ở một số nơi trước đây (không ai từng cung cấp đủ chi tiết, mặc dù, đó là động lực chính để đặt câu hỏi này). Mặc dù, có thể là tôi đã không đủ chi tiết trong câu hỏi ban đầu của mình, đó là lý do tại sao tôi đã cập nhật rộng rãi (và tôi sẽ tiếp tục cập nhật nó nếu tâm trí tôi vấp phải điều gì đó).
TravisG

Cũng không có ý định phạm tội: P
TravisG

@TravisG Thường có các hệ thống phụ thuộc vào các hệ thống khác như Patrick đã chỉ ra. Để tránh sự chậm trễ của khung hoặc để tránh nhiều lần cập nhật như là một phần của bước logic, giải pháp được chấp nhận là tuần tự hóa giai đoạn cập nhật, chạy song song các hệ thống con khi có thể, tuần tự hóa các hệ thống con với các phụ thuộc trong khi bó các bản cập nhật nhỏ hơn đi vào bên trong mỗi hệ thống con sử dụng khái niệmallel_for (). Đó là lý tưởng cho bất kỳ sự kết hợp của nhu cầu cập nhật hệ thống con và linh hoạt nhất.
Naros

0

Tôi đã nghe nói về một giải pháp thú vị cho vấn đề này: Ý tưởng là sẽ có 2 bản sao dữ liệu thực thể (lãng phí, tôi biết). Một bản sao sẽ là bản sao hiện tại và bản còn lại sẽ là bản sao trong quá khứ. Bản sao hiện tại chỉ được viết một cách nghiêm ngặt và bản sao quá khứ chỉ được đọc một cách nghiêm ngặt. Tôi giả định rằng các hệ thống không muốn ghi vào cùng một thành phần dữ liệu, nhưng nếu không phải vậy, các hệ thống đó sẽ nằm trên cùng một luồng. Mỗi luồng sẽ có quyền truy cập ghi vào các bản sao hiện tại của các phần dữ liệu loại trừ lẫn nhau và mọi luồng có quyền truy cập đọc vào tất cả các bản sao trước đây của dữ liệu và do đó có thể cập nhật các bản sao hiện tại bằng cách sử dụng dữ liệu từ các bản sao trước đó khóa. Giữa mỗi khung, bản sao hiện tại trở thành bản sao trong quá khứ, tuy nhiên bạn muốn xử lý việc hoán đổi vai trò.

Phương pháp này cũng loại bỏ các điều kiện chủng tộc vì tất cả các hệ thống sẽ hoạt động với trạng thái cũ sẽ không thay đổi trước / sau khi hệ thống xử lý nó.


Đó là thủ thuật sao chép heap của John Carmack, phải không? Tôi đã tự hỏi về nó, nhưng nó vẫn có khả năng có cùng một vấn đề là nhiều luồng có thể ghi vào cùng một vị trí đầu ra. Đây có thể là một giải pháp tốt nếu bạn giữ mọi thứ "một lượt", nhưng tôi không chắc nó khả thi đến mức nào.
TravisG

Độ trễ hiển thị đầu vào màn hình sẽ tăng thêm 1 khung hình, bao gồm cả độ phản ứng GUI. Điều này có thể quan trọng đối với các trò chơi hành động / thời gian hoặc các thao tác GUI nặng như RTS. Tôi thích nó như một ý tưởng sáng tạo, tuy nhiên.
Patrick Hughes

Tôi đã nghe về điều này từ một người bạn và không biết đó là một trò lừa bịp. Tùy thuộc vào cách kết xuất được thực hiện, kết xuất các thành phần có thể là một khung phía sau. Bạn chỉ có thể sử dụng điều này cho giai đoạn Cập nhật, sau đó kết xuất từ ​​bản sao hiện tại một khi mọi thứ được cập nhật.
John McDonald

0

Tôi biết 3 thiết kế phần mềm xử lý xử lý dữ liệu song song:

  1. Xử lý dữ liệu tuần tự : Điều này nghe có vẻ tồi tệ hơn vì chúng tôi muốn xử lý dữ liệu bằng nhiều luồng. Tuy nhiên, hầu hết các kịch bản yêu cầu nhiều luồng chỉ để hoàn thành công việc trong khi các luồng khác chờ hoặc thực hiện các hoạt động chạy dài. Sử dụng phổ biến nhất là các luồng UI cập nhật giao diện người dùng trong một luồng, trong khi các luồng khác có thể chạy trong nền, nhưng không được phép truy cập trực tiếp vào các thành phần UI. Để truyền kết quả từ các luồng nền, hàng đợi công việc được sử dụng sẽ được xử lý bởi luồng duy nhất ở cơ hội hợp lý tiếp theo.
  2. Đồng bộ hóa truy cập dữ liệu: đây là cách phổ biến nhất để xử lý nhiều luồng truy cập cùng một dữ liệu. Hầu hết các ngôn ngữ lập trình đã xây dựng các lớp và công cụ để khóa các phần trong đó dữ liệu được đọc và / hoặc được viết bởi nhiều luồng đồng thời. Tuy nhiên, cần thận trọng để không chặn hoạt động. Mặt khác, phương pháp này tốn rất nhiều chi phí trong các ứng dụng thời gian thực.
  3. Xử lý các sửa đổi đồng thời chỉ khi chúng xảy ra: phương pháp lạc quan này có thể được thực hiện nếu va chạm hiếm khi xảy ra. Dữ liệu sẽ được đọc và sửa đổi nếu không có nhiều quyền truy cập, nhưng có một cơ chế phát hiện khi dữ liệu được cập nhật đồng thời. Nếu điều đó xảy ra, tính toán đơn sẽ được thực hiện lại cho đến khi thành công.

Dưới đây là một số ví dụ cho mỗi phương pháp có thể được sử dụng trong một hệ thống thực thể:

  1. Hãy nghĩ về CollisionSystemviệc đọc PositionRigidBodycác thành phần và nên cập nhật a Velocity. Thay vì thao túng Velocitytrực tiếp, CollisionSystemthay vào đó , sẽ đưa một hàng CollisionEventvào công việc của một EventSystem. Sự kiện này sau đó sẽ được xử lý tuần tự với các cập nhật khác cho Velocity.
  2. An EntitySystemđịnh nghĩa một tập hợp các thành phần mà nó cần đọc và viết. Đối với mỗi Entitynó sẽ chứa một khóa đọc cho từng thành phần mà nó muốn đọc và một khóa ghi cho từng thành phần mà nó muốn cập nhật. Như thế này, mọi người EntitySystemsẽ có thể đọc đồng thời các thành phần trong khi các hoạt động cập nhật được đồng bộ hóa.
  3. Lấy ví dụ về MovementSystem, Positionthành phần này là bất biến và chứa số sửa đổi . Các MovementSystemSavely đọc PositionVelocitythành phần và tính toán mới Position, incrementing đọc sửa đổi số lượng và cố gắng cập nhật các Positionthành phần. Trong trường hợp sửa đổi đồng thời, khung cho biết điều này trên bản cập nhật và Entitysẽ được đưa trở lại vào danh sách các thực thể phải được cập nhật bởi MovementSystem.

Tùy thuộc vào hệ thống, thực thể và khoảng thời gian cập nhật, mỗi cách tiếp cận có thể tốt hoặc xấu. Một khung hệ thống thực thể có thể cho phép người dùng lựa chọn giữa các tùy chọn đó để điều chỉnh hiệu suất.

Tôi hy vọng tôi có thể thêm một số ý tưởng vào cuộc thảo luận và xin vui lòng cho tôi biết nếu có một số tin tức về nó.

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.