Làm thế nào là hệ thống thực thể bộ nhớ cache hiệu quả?


32

Gần đây, tôi đã đọc rất nhiều hệ thống thực thể để triển khai trong công cụ trò chơi C ++ / OpenGL của mình. Hai lợi ích chính mà tôi liên tục nghe thấy được ca ngợi về các hệ thống thực thể là

  1. việc xây dựng dễ dàng các loại thực thể mới, do không phải vướng vào hệ thống phân cấp thừa kế phức tạp và
  2. hiệu quả bộ nhớ cache, mà tôi đang gặp khó khăn để hiểu.

Lý thuyết là đơn giản, tất nhiên; mỗi thành phần được lưu trữ liên tục trong một khối bộ nhớ, vì vậy hệ thống quan tâm đến thành phần đó có thể lặp lại toàn bộ danh sách mà không cần phải nhảy xung quanh trong bộ nhớ và giết bộ đệm. Vấn đề là tôi thực sự không thể nghĩ ra một tình huống mà điều này thực sự thiết thực.


Trước tiên, hãy xem cách các thành phần được lưu trữ và cách chúng tham chiếu lẫn nhau. Các hệ thống cần có khả năng làm việc với nhiều hơn một thành phần, tức là cả hệ thống kết xuất và vật lý cần truy cập vào thành phần biến đổi. Tôi đã thấy một số triển khai có thể giải quyết vấn đề này và không ai trong số họ thực hiện tốt điều đó.

Bạn có thể có các thành phần lưu trữ các con trỏ tới các thành phần khác hoặc các con trỏ tới các thực thể lưu trữ các con trỏ tới các thành phần. Tuy nhiên, ngay sau khi bạn ném con trỏ vào hỗn hợp, bạn đã giết hiệu quả bộ đệm. Bạn có thể đảm bảo rằng mọi mảng thành phần đều lớn, trong đó 'n' là số lượng thực thể còn tồn tại trong hệ thống, nhưng cách tiếp cận này vô cùng lãng phí bộ nhớ; điều này làm cho việc thêm các loại thành phần mới vào công cụ rất khó khăn, nhưng vẫn làm giảm hiệu quả bộ đệm, bởi vì bạn đang nhảy từ mảng này sang mảng khác. Bạn có thể xen kẽ mảng thực thể của mình, thay vì giữ các mảng riêng biệt, nhưng bạn vẫn lãng phí bộ nhớ; làm cho nó rất tốn kém để thêm các thành phần hoặc hệ thống mới, nhưng bây giờ với lợi ích bổ sung là vô hiệu hóa tất cả các cấp độ cũ của bạn và lưu tệp.

Đây là tất cả giả định rằng các thực thể được xử lý tuyến tính trong một danh sách, mỗi khung hoặc đánh dấu. Trong thực tế, điều này không thường xuyên xảy ra. Giả sử bạn sử dụng trình kết xuất ngành / cổng thông tin hoặc octree để thực hiện loại bỏ tắc. Bạn có thể lưu trữ các thực thể liên tục trong một khu vực / nút, nhưng bạn sẽ nhảy xung quanh, cho dù bạn có thích hay không. Sau đó, bạn có các hệ thống khác, có thể thích các thực thể được lưu trữ theo một số thứ tự khác. AI có thể ổn với việc lưu trữ các thực thể trong một danh sách lớn, cho đến khi bạn bắt đầu làm việc với AI LOD; sau đó, bạn sẽ muốn chia danh sách đó theo khoảng cách cho người chơi hoặc một số số liệu LOD khác. Vật lý sẽ muốn sử dụng octree đó. Các kịch bản không quan tâm, chúng cần phải chạy, không có vấn đề gì.

Tôi có thể thấy các thành phần phân tách giữa "logic" (ví dụ ai, script, v.v.) và "world" (ví dụ: kết xuất, vật lý, âm thanh, v.v.) và quản lý từng danh sách riêng biệt, nhưng các danh sách này vẫn phải tương tác với nhau. AI là vô nghĩa, nếu nó không thể ảnh hưởng đến trạng thái biến đổi hoặc hoạt hình được sử dụng để hiển thị của một thực thể.


Làm thế nào các hệ thống thực thể "hiệu quả bộ nhớ cache" trong một công cụ trò chơi trong thế giới thực? Có lẽ có một cách tiếp cận hỗn hợp mà mọi người đang sử dụng, nhưng không nói đến, như lưu trữ các thực thể trong một mảng trên toàn cầu và tham chiếu nó trong phạm vi tám?


Xin lưu ý rằng ngày nay bạn có CPU đa lõi và bộ nhớ cache lớn hơn một hàng. Ngay cả khi bạn cần thông tin truy cập từ hai hệ thống, chúng vẫn có khả năng phù hợp với cả hai. Cũng lưu ý, kết xuất đồ họa thường được tách riêng - chính xác cho những gì bạn đã nêu (cây, cảnh, ..)
wonderra

2
Các hệ thống thực thể không phải lúc nào cũng hiệu quả bộ nhớ cache, nhưng nó có thể là một lợi thế của một số triển khai (hơn các cách khác để thực hiện những điều tương tự).
Josh

Câu trả lời:


43

Hai lợi ích chính mà tôi liên tục nghe thấy được khen ngợi về các hệ thống thực thể là 1) việc xây dựng dễ dàng các loại thực thể mới do không phải vướng vào hệ thống phân cấp thừa kế phức tạp và 2) hiệu quả bộ đệm.

Lưu ý rằng (1) là một lợi ích của thiết kế dựa trên thành phần , không chỉ ES / ECS. Bạn có thể sử dụng các thành phần theo nhiều cách không có phần "hệ thống" và chúng hoạt động tốt (và nhiều trò chơi indie và AAA sử dụng các kiến ​​trúc như vậy).

Mô hình đối tượng Unity tiêu chuẩn (sử dụng GameObjectMonoBehaviourđối tượng) không phải là ECS, mà là thiết kế dựa trên thành phần. Tất nhiên, tính năng Unity ECS mới là một ECS thực tế.

Các hệ thống cần có khả năng làm việc với nhiều hơn một thành phần, tức là cả hệ thống kết xuất và vật lý cần truy cập vào thành phần biến đổi.

Một số ECS sắp xếp các bộ chứa thành phần của chúng theo ID thực thể, nghĩa là các thành phần tương ứng trong mỗi nhóm sẽ theo cùng một thứ tự.

Điều này có nghĩa là nếu bạn lặp tuyến tính trên thành phần đồ họa, bạn cũng lặp lại tuyến tính trên các thành phần biến đổi tương ứng. Bạn có thể bỏ qua một số biến đổi (vì bạn có thể có các khối kích hoạt vật lý mà bạn không kết xuất hoặc tương tự) nhưng vì bạn luôn bỏ qua về phía trước trong bộ nhớ (và thông thường không phải là khoảng cách rất lớn), bạn vẫn sẽ đi để đạt được hiệu quả.

Điều này tương tự như cách bạn có Cấu trúc mảng (SOA) là cách tiếp cận được đề xuất cho HPC. CPU và bộ đệm có thể xử lý nhiều mảng tuyến tính gần như cũng có thể xử lý một mảng tuyến tính duy nhất và tốt hơn nhiều so với khả năng truy cập bộ nhớ ngẫu nhiên.

Một chiến lược khác được sử dụng trong một số triển khai ECS - bao gồm Unity ECS - là phân bổ các Thành phần dựa trên Archetype của Thực thể tương ứng của chúng. Đó là, tất cả các thực thể với một cách chính xác các bộ Components ( PhysicsBody, Transform) sẽ được phân bổ một cách riêng biệt từ Entities với các thành phần khác nhau (ví dụ PhysicsBody, Transform, Renderable ).

Các hệ thống trong các thiết kế như vậy hoạt động bằng cách trước tiên tìm tất cả các Archetype phù hợp với yêu cầu của chúng (có bộ Thành phần bắt buộc), lặp lại danh sách Archetypes đó và lặp lại các Thành phần được lưu trữ trong mỗi Archetype phù hợp. Điều này cho phép truy cập thành phần O (1) hoàn toàn tuyến tính và thực trong Archetype và cho phép Hệ thống tìm các Thực thể tương thích với chi phí rất thấp (bằng cách tìm kiếm một danh sách nhỏ Archetypes thay vì tìm kiếm hàng trăm nghìn Thực thể).

Bạn có thể có các thành phần lưu trữ con trỏ tới các thành phần khác hoặc con trỏ tới các thực thể lưu trữ con trỏ tới các thành phần.

Các thành phần tham chiếu các thành phần khác trên cùng một thực thể không cần lưu trữ bất cứ thứ gì. Để tham chiếu các thành phần trên các thực thể khác, chỉ cần lưu trữ ID thực thể.

Nếu một thành phần được phép tồn tại nhiều lần cho một thực thể và bạn cần tham chiếu một thể hiện cụ thể, lưu trữ ID của thực thể khác và chỉ mục thành phần cho thực thể đó. Tuy nhiên, nhiều triển khai ECS không cho phép trường hợp này, đặc biệt vì nó làm cho các hoạt động này kém hiệu quả hơn.

Bạn có thể đảm bảo rằng mọi mảng thành phần đều lớn, trong đó 'n' là số lượng thực thể còn tồn tại trong hệ thống

Sử dụng các thẻ điều khiển (ví dụ: chỉ mục + đánh dấu thế hệ) và không phải con trỏ và sau đó bạn có thể thay đổi kích thước các mảng mà không sợ phá vỡ các tham chiếu đối tượng.

Bạn cũng có thể sử dụng cách tiếp cận "mảng chunked" (một mảng các mảng) tương tự như nhiều cách std::dequetriển khai phổ biến (mặc dù không có kích thước khối nhỏ đáng kể của các triển khai đã nói) nếu bạn muốn cho phép con trỏ vì một số lý do hoặc nếu bạn đã đo lường vấn đề với mảng thay đổi kích thước hiệu suất.

Thứ hai, đây là tất cả giả định rằng các thực thể được xử lý tuyến tính trong một danh sách mỗi khung hình / đánh dấu, nhưng trong thực tế, điều này không thường xảy ra

Nó phụ thuộc vào thực thể. Có, đối với nhiều trường hợp sử dụng, nó không đúng. Thật vậy, đây là lý do tại sao tôi nhấn mạnh mạnh mẽ sự khác biệt giữa thiết kế dựa trên thành phần (tốt) và hệ thống thực thể (một hình thức cụ thể của CBD).

Một số thành phần của bạn chắc chắn sẽ dễ dàng xử lý tuyến tính. Ngay cả trong các trường hợp sử dụng "cây nặng" thông thường, chúng tôi chắc chắn đã thấy hiệu suất tăng từ việc sử dụng các mảng được đóng gói chặt chẽ (chủ yếu trong các trường hợp liên quan đến N vài trăm, như các tác nhân AI trong một trò chơi thông thường).

Một số nhà phát triển cũng nhận thấy rằng những lợi thế về hiệu suất của việc sử dụng các cấu trúc dữ liệu được phân bổ tuyến tính theo định hướng dữ liệu vượt xa lợi thế về hiệu suất của việc sử dụng các cấu trúc dựa trên cây "thông minh hơn". Tất cả phụ thuộc vào trò chơi và các trường hợp sử dụng cụ thể, tất nhiên.

Giả sử bạn sử dụng trình kết xuất sector / cổng thông tin hoặc octree để thực hiện loại bỏ tắc. Bạn có thể lưu trữ các thực thể liên tục trong một khu vực / nút, nhưng bạn sẽ nhảy xung quanh dù bạn có thích hay không.

Bạn sẽ ngạc nhiên bao nhiêu mảng vẫn giúp. Bạn đang nhảy xung quanh trong một vùng bộ nhớ nhỏ hơn nhiều so với "bất cứ nơi nào" và thậm chí với tất cả các bước nhảy, bạn vẫn có nhiều khả năng kết thúc một thứ gì đó trong bộ nhớ cache. Với một cây có kích thước nhất định hoặc ít hơn, bạn thậm chí có thể tìm nạp trước toàn bộ vào bộ đệm và không bao giờ bỏ lỡ bộ đệm trên cây đó.

Ngoài ra còn có các cấu trúc cây được xây dựng để sống trong các mảng được đóng gói chặt chẽ. Chẳng hạn, với quãng tám của bạn, bạn có thể sử dụng cấu trúc giống như đống (cha mẹ trước con cái, anh chị em cạnh nhau) và đảm bảo rằng ngay cả khi bạn "đi sâu" vào cây bạn luôn lặp đi lặp lại trong mảng, điều này giúp CPU tối ưu hóa truy cập bộ nhớ / tra cứu bộ nhớ cache.

Đó là một điểm quan trọng để thực hiện. CPU x86 là một con thú phức tạp. CPU đang chạy một trình tối ưu hóa vi mã trên mã máy của bạn một cách hiệu quả, chia nó thành các vi lệnh nhỏ hơn và sắp xếp lại các hướng dẫn, dự đoán các mẫu truy cập bộ nhớ, v.v. Các mẫu truy cập dữ liệu quan trọng hơn có thể dễ hiểu nếu tất cả những gì bạn có là sự hiểu biết ở mức độ cao CPU hoặc bộ đệm hoạt động như thế nào.

Sau đó, bạn có các hệ thống khác, có thể thích các thực thể được lưu trữ theo một số thứ tự khác.

Bạn có thể lưu trữ chúng nhiều lần. Khi bạn loại bỏ các mảng của mình xuống các chi tiết tối thiểu, bạn có thể thấy bạn thực sự tiết kiệm bộ nhớ (vì bạn đã loại bỏ các con trỏ 64 bit của mình và có thể sử dụng các chỉ số nhỏ hơn) với phương pháp này.

Bạn có thể xen kẽ mảng thực thể của mình thay vì giữ các mảng riêng biệt, nhưng bạn vẫn lãng phí bộ nhớ

Điều này là phản đối với việc sử dụng bộ nhớ cache tốt. Nếu tất cả những gì bạn quan tâm là các biến đổi và dữ liệu đồ họa, tại sao làm cho máy dành thời gian để lấy tất cả các dữ liệu khác cho vật lý và AI và nhập và gỡ lỗi, v.v.

Đó là điểm thường được đưa ra có lợi cho ECS so với các đối tượng trò chơi nguyên khối (mặc dù không thực sự áp dụng khi so sánh với các kiến ​​trúc dựa trên thành phần khác).

Đối với những gì nó có giá trị, hầu hết các triển khai ECS "cấp sản xuất" mà tôi biết sử dụng lưu trữ xen kẽ. Cách tiếp cận Archetype phổ biến mà tôi đã đề cập trước đó (ví dụ được sử dụng trong Unity ECS) được xây dựng rất rõ ràng để sử dụng lưu trữ xen kẽ cho các Thành phần được liên kết với Archetype.

AI là vô nghĩa nếu nó không thể ảnh hưởng đến trạng thái biến đổi hoặc hoạt hình được sử dụng để hiển thị của một thực thể.

Chỉ vì AI không thể truy cập một cách hiệu quả dữ liệu biến đổi tuyến tính không có nghĩa là không có hệ thống nào khác có thể sử dụng tối ưu hóa bố cục dữ liệu đó một cách hiệu quả. Bạn có thể sử dụng một mảng đóng gói để chuyển đổi dữ liệu mà không ngăn các hệ thống logic trò chơi thực hiện mọi thứ theo cách thông thường mà các hệ thống logic trò chơi thường làm.

Bạn cũng đang quên bộ đệm mã . Khi bạn sử dụng cách tiếp cận hệ thống của ECS (không giống như một số kiến ​​trúc thành phần ngây thơ hơn), bạn sẽ đảm bảo rằng bạn đang chạy cùng một vòng mã nhỏ và không nhảy qua lại các bảng chức năng ảo để phân loại các Updatehàm ngẫu nhiên rải rác khắp nơi nhị phân của bạn. Vì vậy, trong trường hợp AI, bạn thực sự muốn giữ tất cả các thành phần AI khác nhau của mình (vì chắc chắn bạn có nhiều hơn một để bạn có thể soạn các hành vi!) Trong các thùng riêng biệt và xử lý từng danh sách riêng biệt để có được cách sử dụng bộ đệm mã tốt nhất.

Với hàng đợi sự kiện bị trì hoãn (trong đó một hệ thống tạo danh sách các sự kiện nhưng không gửi chúng cho đến khi hệ thống xử lý xong tất cả các thực thể), bạn có thể đảm bảo rằng bộ đệm mã của bạn được sử dụng tốt trong khi vẫn giữ các sự kiện.

Sử dụng một cách tiếp cận trong đó mỗi hệ thống biết hàng đợi sự kiện nào cần đọc ra cho khung, bạn thậm chí có thể thực hiện việc đọc các sự kiện nhanh chóng. Hoặc nhanh hơn không có, ít nhất.

Hãy nhớ rằng, hiệu suất không phải là tuyệt đối. Bạn không cần phải loại bỏ mọi lỗi bộ nhớ cache cuối cùng để bắt đầu thấy lợi ích hiệu suất của thiết kế hướng dữ liệu tốt.

Vẫn còn có nghiên cứu tích cực để làm cho nhiều hệ thống trò chơi hoạt động tốt hơn với kiến ​​trúc ECS và các mẫu thiết kế hướng dữ liệu. Tương tự như một số điều tuyệt vời mà chúng ta đã thấy với SIMD trong những năm gần đây (ví dụ: trình phân tích cú pháp JSON), chúng ta đang thấy ngày càng nhiều thứ được thực hiện với kiến ​​trúc ECS dường như không trực quan với kiến ​​trúc trò chơi cổ điển nhưng cung cấp một số lợi ích (tốc độ, đa luồng, khả năng kiểm tra, vv).

Hoặc có lẽ có một cách tiếp cận hỗn hợp mà mọi người đang sử dụng nhưng không ai nói về

Đây là những gì tôi đã ủng hộ trong quá khứ, đặc biệt là đối với những người hoài nghi về kiến ​​trúc ECS: sử dụng các phương pháp tiếp cận định hướng dữ liệu tốt cho các thành phần có hiệu suất rất quan trọng. Sử dụng kiến ​​trúc đơn giản hơn, nơi đơn giản cải thiện thời gian phát triển. Không bấm còi mỗi thành phần đơn lẻ thành một định nghĩa quá mức nghiêm ngặt về thành phần hóa như ECS đề xuất. Phát triển kiến ​​trúc thành phần của bạn theo cách mà bạn có thể dễ dàng sử dụng các cách tiếp cận giống như ECS trong đó chúng có ý nghĩa và sử dụng cấu trúc thành phần đơn giản hơn trong đó cách tiếp cận giống như ECS không có ý nghĩa (hoặc ít ý nghĩa hơn cấu trúc cây, v.v.) .

Cá nhân tôi là một người chuyển đổi tương đối gần đây thành sức mạnh thực sự của ECS. Mặc dù đối với tôi, yếu tố quyết định là điều hiếm khi được đề cập về ECS: nó làm cho các bài kiểm tra viết cho các hệ thống trò chơi và logic gần như không đáng kể so với các thiết kế dựa trên thành phần logic được kết hợp chặt chẽ mà tôi đã làm việc trước đây. Do các kiến ​​trúc ECS đưa tất cả logic vào Hệ thống, vốn chỉ tiêu thụ Thành phần và tạo ra các bản cập nhật Thành phần, nên việc xây dựng một bộ Thành phần "giả" để kiểm tra hành vi của Hệ thống khá dễ dàng; bởi vì hầu hết logic trò chơi chỉ nên tồn tại bên trong Hệ thống, điều đó có nghĩa là việc kiểm tra tất cả các Hệ thống của bạn sẽ cung cấp độ bao phủ mã khá cao cho logic trò chơi của bạn. Các hệ thống có thể sử dụng các phụ thuộc giả (ví dụ: giao diện GPU) cho các thử nghiệm với mức độ phức tạp hoặc tác động hiệu suất ít hơn nhiều so với bạn '

Bên cạnh đó, bạn có thể lưu ý rằng rất nhiều người nói về ECS mà không thực sự hiểu nó là gì. Tôi thấy Unity cổ điển được gọi là ECS với tần suất giảm, minh họa rằng có quá nhiều nhà phát triển trò chơi đánh đồng "ECS" với "Thành phần" và hoàn toàn bỏ qua phần "Hệ thống thực thể". Bạn thấy rất nhiều tình yêu chất đống trên ECS trên Internet khi một phần lớn mọi người thực sự chỉ ủng hộ thiết kế dựa trên thành phần, chứ không phải ECS thực tế. Tại thời điểm này, gần như vô nghĩa để tranh luận về nó; ECS đã bị hỏng từ nghĩa gốc của nó thành một thuật ngữ chung và bạn cũng có thể chấp nhận rằng "ECS" không có nghĩa tương tự như "ECS hướng dữ liệu". : /


1
Sẽ rất hữu ích khi xác định (hoặc liên kết đến) ý nghĩa của ECS, nếu bạn sẽ so sánh / đối chiếu nó với thiết kế dựa trên thành phần chung. Tôi cho một người không rõ ràng về sự khác biệt là gì. :)
Nathan Reed

Cảm ơn rất nhiều về câu trả lời, có vẻ như tôi vẫn còn rất nhiều nghiên cứu để làm về chủ đề này. Có cuốn sách nào bạn có thể chỉ cho tôi không?
Haydn V. Harach

3
@NathanReed: ECS được ghi lại ở những nơi như entity-systems.wikidot.com/es-terminology . Thiết kế dựa trên thành phần chỉ là sự kế thừa tổng hợp của ol 'thông thường nhưng tập trung vào thành phần động hữu ích cho thiết kế trò chơi. Bạn có thể viết các công cụ dựa trên thành phần không sử dụng Hệ thống hoặc Thực thể (theo nghĩa của thuật ngữ ECS) và bạn có thể sử dụng các thành phần nhiều hơn trong công cụ trò chơi thay vì chỉ các đối tượng / đối tượng trò chơi, đó là lý do tôi nhấn mạnh sự khác biệt.
Sean Middleditch

2
Đây là một trong những bài viết hay nhất về ECS mà tôi từng đọc, bất chấp tất cả các tài liệu trên web. Mega giơ ngón tay cái lên. Vì vậy, Sean, cuối cùng, cách tiếp cận chung của bạn để phát triển trò chơi (chứ không phải là phức tạp)? Một ECS thuần túy? Một cách tiếp cận hỗn hợp giữa dựa trên thành phần và ECS? Tôi muốn biết thêm về thiết kế của bạn! Có phải nó đòi hỏi quá nhiều để có được bạn trong Skype hoặc một cái gì đó khác để thảo luận về điều này?
Grimshaw

2
@Grimshaw: gamedev.net là một nơi thích hợp cho các cuộc thảo luận kết thúc cởi mở hơn, như là reddit.com/r/gamedev Tôi cho rằng (mặc dù bản thân tôi không phải là một redditer). Tôi thường xuyên trên gamedev.net, cũng như nhiều người thông minh khác. Tôi thường không nói chuyện một đối một; Tôi khá bận rộn và thích thời gian chết của tôi (tức là biên dịch) được dành để giúp đỡ nhiều người hơn là số ít. :)
Sean Middleditch
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.